zombienet_configuration/
relaychain.rs

1use std::{cell::RefCell, error::Error, fmt::Debug, marker::PhantomData, rc::Rc};
2
3use serde::{Deserialize, Serialize};
4use support::constants::{DEFAULT_TYPESTATE, THIS_IS_A_BUG};
5
6use crate::{
7    shared::{
8        errors::{ConfigError, FieldError},
9        helpers::{merge_errors, merge_errors_vecs},
10        macros::states,
11        node::{self, GroupNodeConfig, GroupNodeConfigBuilder, NodeConfig, NodeConfigBuilder},
12        resources::{Resources, ResourcesBuilder},
13        types::{
14            Arg, AssetLocation, Chain, ChainDefaultContext, Command, Image, ValidationContext,
15        },
16    },
17    types::JsonOverrides,
18    utils::{default_command_polkadot, default_relaychain_chain, is_false},
19};
20
21/// A relay chain configuration, composed of nodes and fine-grained configuration options.
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub struct RelaychainConfig {
24    #[serde(default = "default_relaychain_chain")]
25    chain: Chain,
26    #[serde(default = "default_command_polkadot")]
27    default_command: Option<Command>,
28    default_image: Option<Image>,
29    default_resources: Option<Resources>,
30    default_db_snapshot: Option<AssetLocation>,
31    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
32    default_args: Vec<Arg>,
33    chain_spec_path: Option<AssetLocation>,
34    // Full _template_ command, will be rendered (using custom token replacements)
35    // and executed for generate the chain-spec.
36    // available tokens {{chainName}} / {{disableBootnodes}}
37    chain_spec_command: Option<String>,
38    #[serde(skip_serializing_if = "is_false", default)]
39    chain_spec_command_is_local: bool,
40    random_nominators_count: Option<u32>,
41    max_nominations: Option<u8>,
42    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
43    nodes: Vec<NodeConfig>,
44    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
45    node_groups: Vec<GroupNodeConfig>,
46    #[serde(rename = "genesis", skip_serializing_if = "Option::is_none")]
47    runtime_genesis_patch: Option<serde_json::Value>,
48    // Path or url to override the runtime (:code) in the chain-spec
49    wasm_override: Option<AssetLocation>,
50    command: Option<Command>,
51    // Inline json or asset location to override raw chainspec
52    raw_spec_override: Option<JsonOverrides>,
53}
54
55impl RelaychainConfig {
56    /// The chain name.
57    pub fn chain(&self) -> &Chain {
58        &self.chain
59    }
60
61    /// The default command used for nodes.
62    pub fn default_command(&self) -> Option<&Command> {
63        self.default_command.as_ref()
64    }
65
66    /// The default container image used for nodes.
67    pub fn default_image(&self) -> Option<&Image> {
68        self.default_image.as_ref()
69    }
70
71    /// The default resources limits used for nodes.
72    pub fn default_resources(&self) -> Option<&Resources> {
73        self.default_resources.as_ref()
74    }
75
76    /// The default database snapshot location that will be used for state.
77    pub fn default_db_snapshot(&self) -> Option<&AssetLocation> {
78        self.default_db_snapshot.as_ref()
79    }
80
81    /// The default arguments that will be used to launch the node command.
82    pub fn default_args(&self) -> Vec<&Arg> {
83        self.default_args.iter().collect::<Vec<&Arg>>()
84    }
85
86    /// The location of an pre-existing chain specification for the relay chain.
87    pub fn chain_spec_path(&self) -> Option<&AssetLocation> {
88        self.chain_spec_path.as_ref()
89    }
90
91    /// The location of a wasm runtime to override in the chain-spec.
92    pub fn wasm_override(&self) -> Option<&AssetLocation> {
93        self.wasm_override.as_ref()
94    }
95
96    /// The full _template_ command to genera the chain-spec
97    pub fn chain_spec_command(&self) -> Option<&str> {
98        self.chain_spec_command.as_deref()
99    }
100
101    /// Does the chain_spec_command needs to be run locally
102    pub fn chain_spec_command_is_local(&self) -> bool {
103        self.chain_spec_command_is_local
104    }
105
106    /// The non-default command used for nodes.
107    pub fn command(&self) -> Option<&Command> {
108        self.command.as_ref()
109    }
110
111    /// The number of `random nominators` to create for chains using staking, this is used in tandem with `max_nominations` to simulate the amount of nominators and nominations.
112    pub fn random_nominators_count(&self) -> Option<u32> {
113        self.random_nominators_count
114    }
115
116    /// The maximum number of nominations to create per nominator.
117    pub fn max_nominations(&self) -> Option<u8> {
118        self.max_nominations
119    }
120
121    /// The genesis overrides as a JSON value.
122    pub fn runtime_genesis_patch(&self) -> Option<&serde_json::Value> {
123        self.runtime_genesis_patch.as_ref()
124    }
125
126    /// The nodes of the relay chain.
127    pub fn nodes(&self) -> Vec<&NodeConfig> {
128        self.nodes.iter().collect::<Vec<&NodeConfig>>()
129    }
130
131    /// The group nodes of the relay chain.
132    pub fn group_node_configs(&self) -> Vec<&GroupNodeConfig> {
133        self.node_groups.iter().collect::<Vec<&GroupNodeConfig>>()
134    }
135
136    /// The location of a file or inline json to override raw chain-spec.
137    pub fn raw_spec_override(&self) -> Option<&JsonOverrides> {
138        self.raw_spec_override.as_ref()
139    }
140
141    pub(crate) fn set_nodes(&mut self, nodes: Vec<NodeConfig>) {
142        self.nodes = nodes;
143    }
144}
145
146states! {
147    Initial,
148    WithChain,
149    WithAtLeastOneNode
150}
151
152/// A relay chain configuration builder, used to build a [`RelaychainConfig`] declaratively with fields validation.
153pub struct RelaychainConfigBuilder<State> {
154    config: RelaychainConfig,
155    validation_context: Rc<RefCell<ValidationContext>>,
156    errors: Vec<anyhow::Error>,
157    _state: PhantomData<State>,
158}
159
160impl Default for RelaychainConfigBuilder<Initial> {
161    fn default() -> Self {
162        Self {
163            config: RelaychainConfig {
164                chain: "default"
165                    .try_into()
166                    .expect(&format!("{DEFAULT_TYPESTATE} {THIS_IS_A_BUG}")),
167                default_command: None,
168                default_image: None,
169                default_resources: None,
170                default_db_snapshot: None,
171                default_args: vec![],
172                chain_spec_path: None,
173                chain_spec_command: None,
174                wasm_override: None,
175                chain_spec_command_is_local: false, // remote cmd by default
176                command: None,
177                random_nominators_count: None,
178                max_nominations: None,
179                runtime_genesis_patch: None,
180                nodes: vec![],
181                node_groups: vec![],
182                raw_spec_override: None,
183            },
184            validation_context: Default::default(),
185            errors: vec![],
186            _state: PhantomData,
187        }
188    }
189}
190
191impl<A> RelaychainConfigBuilder<A> {
192    fn transition<B>(
193        config: RelaychainConfig,
194        validation_context: Rc<RefCell<ValidationContext>>,
195        errors: Vec<anyhow::Error>,
196    ) -> RelaychainConfigBuilder<B> {
197        RelaychainConfigBuilder {
198            config,
199            validation_context,
200            errors,
201            _state: PhantomData,
202        }
203    }
204
205    fn default_chain_context(&self) -> ChainDefaultContext {
206        ChainDefaultContext {
207            default_command: self.config.default_command.clone(),
208            default_image: self.config.default_image.clone(),
209            default_resources: self.config.default_resources.clone(),
210            default_db_snapshot: self.config.default_db_snapshot.clone(),
211            default_args: self.config.default_args.clone(),
212        }
213    }
214
215    fn create_node_builder<F>(&self, f: F) -> NodeConfigBuilder<node::Buildable>
216    where
217        F: FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
218    {
219        f(NodeConfigBuilder::new(
220            self.default_chain_context(),
221            self.validation_context.clone(),
222        ))
223    }
224}
225
226impl RelaychainConfigBuilder<Initial> {
227    pub fn new(
228        validation_context: Rc<RefCell<ValidationContext>>,
229    ) -> RelaychainConfigBuilder<Initial> {
230        Self {
231            validation_context,
232            ..Self::default()
233        }
234    }
235
236    /// Set the chain name (e.g. rococo-local).
237    pub fn with_chain<T>(self, chain: T) -> RelaychainConfigBuilder<WithChain>
238    where
239        T: TryInto<Chain>,
240        T::Error: Error + Send + Sync + 'static,
241    {
242        match chain.try_into() {
243            Ok(chain) => Self::transition(
244                RelaychainConfig {
245                    chain,
246                    ..self.config
247                },
248                self.validation_context,
249                self.errors,
250            ),
251            Err(error) => Self::transition(
252                self.config,
253                self.validation_context,
254                merge_errors(self.errors, FieldError::Chain(error.into()).into()),
255            ),
256        }
257    }
258}
259
260impl RelaychainConfigBuilder<WithChain> {
261    /// Set the default command used for nodes. Can be overridden.
262    pub fn with_default_command<T>(self, command: T) -> Self
263    where
264        T: TryInto<Command>,
265        T::Error: Error + Send + Sync + 'static,
266    {
267        match command.try_into() {
268            Ok(command) => Self::transition(
269                RelaychainConfig {
270                    default_command: Some(command),
271                    ..self.config
272                },
273                self.validation_context,
274                self.errors,
275            ),
276            Err(error) => Self::transition(
277                self.config,
278                self.validation_context,
279                merge_errors(self.errors, FieldError::DefaultCommand(error.into()).into()),
280            ),
281        }
282    }
283
284    /// Set the default container image used for nodes. Can be overridden.
285    pub fn with_default_image<T>(self, image: T) -> Self
286    where
287        T: TryInto<Image>,
288        T::Error: Error + Send + Sync + 'static,
289    {
290        match image.try_into() {
291            Ok(image) => Self::transition(
292                RelaychainConfig {
293                    default_image: Some(image),
294                    ..self.config
295                },
296                self.validation_context,
297                self.errors,
298            ),
299            Err(error) => Self::transition(
300                self.config,
301                self.validation_context,
302                merge_errors(self.errors, FieldError::DefaultImage(error.into()).into()),
303            ),
304        }
305    }
306
307    /// Set the default resources limits used for nodes. Can be overridden.
308    pub fn with_default_resources(
309        self,
310        f: impl FnOnce(ResourcesBuilder) -> ResourcesBuilder,
311    ) -> Self {
312        match f(ResourcesBuilder::new()).build() {
313            Ok(default_resources) => Self::transition(
314                RelaychainConfig {
315                    default_resources: Some(default_resources),
316                    ..self.config
317                },
318                self.validation_context,
319                self.errors,
320            ),
321            Err(errors) => Self::transition(
322                self.config,
323                self.validation_context,
324                merge_errors_vecs(
325                    self.errors,
326                    errors
327                        .into_iter()
328                        .map(|error| FieldError::DefaultResources(error).into())
329                        .collect::<Vec<_>>(),
330                ),
331            ),
332        }
333    }
334
335    /// Set the default database snapshot location that will be used for state. Can be overridden.
336    pub fn with_default_db_snapshot(self, location: impl Into<AssetLocation>) -> Self {
337        Self::transition(
338            RelaychainConfig {
339                default_db_snapshot: Some(location.into()),
340                ..self.config
341            },
342            self.validation_context,
343            self.errors,
344        )
345    }
346
347    /// Set the default arguments that will be used to execute the node command. Can be overridden.
348    pub fn with_default_args(self, args: Vec<Arg>) -> Self {
349        Self::transition(
350            RelaychainConfig {
351                default_args: args,
352                ..self.config
353            },
354            self.validation_context,
355            self.errors,
356        )
357    }
358
359    /// Set the location of a pre-existing chain specification for the relay chain.
360    pub fn with_chain_spec_path(self, location: impl Into<AssetLocation>) -> Self {
361        Self::transition(
362            RelaychainConfig {
363                chain_spec_path: Some(location.into()),
364                ..self.config
365            },
366            self.validation_context,
367            self.errors,
368        )
369    }
370
371    /// Set the location of a wasm to override the chain-spec.
372    pub fn with_wasm_override(self, location: impl Into<AssetLocation>) -> Self {
373        Self::transition(
374            RelaychainConfig {
375                wasm_override: Some(location.into()),
376                ..self.config
377            },
378            self.validation_context,
379            self.errors,
380        )
381    }
382
383    /// Set the chain-spec command _template_ for the relay chain.
384    pub fn with_chain_spec_command(self, cmd_template: impl Into<String>) -> Self {
385        Self::transition(
386            RelaychainConfig {
387                chain_spec_command: Some(cmd_template.into()),
388                ..self.config
389            },
390            self.validation_context,
391            self.errors,
392        )
393    }
394
395    /// Set if the chain-spec command needs to be run locally or not (false by default)
396    pub fn chain_spec_command_is_local(self, choice: bool) -> Self {
397        Self::transition(
398            RelaychainConfig {
399                chain_spec_command_is_local: choice,
400                ..self.config
401            },
402            self.validation_context,
403            self.errors,
404        )
405    }
406
407    /// Set the number of `random nominators` to create for chains using staking, this is used in tandem with `max_nominations` to simulate the amount of nominators and nominations.
408    pub fn with_random_nominators_count(self, random_nominators_count: u32) -> Self {
409        Self::transition(
410            RelaychainConfig {
411                random_nominators_count: Some(random_nominators_count),
412                ..self.config
413            },
414            self.validation_context,
415            self.errors,
416        )
417    }
418
419    /// Set the maximum number of nominations to create per nominator.
420    pub fn with_max_nominations(self, max_nominations: u8) -> Self {
421        Self::transition(
422            RelaychainConfig {
423                max_nominations: Some(max_nominations),
424                ..self.config
425            },
426            self.validation_context,
427            self.errors,
428        )
429    }
430
431    /// Set the genesis overrides as a JSON object.
432    pub fn with_genesis_overrides(self, genesis_overrides: impl Into<serde_json::Value>) -> Self {
433        Self::transition(
434            RelaychainConfig {
435                runtime_genesis_patch: Some(genesis_overrides.into()),
436                ..self.config
437            },
438            self.validation_context,
439            self.errors,
440        )
441    }
442
443    /// Add a new validator node using a nested [`NodeConfigBuilder`].
444    /// The node will be configured as a validator (authority) with the --validator flag.
445    pub fn with_validator(
446        self,
447        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
448    ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
449        match self.create_node_builder(f).validator(true).build() {
450            Ok(node) => Self::transition(
451                RelaychainConfig {
452                    nodes: [self.config.nodes, vec![node]].concat(),
453                    ..self.config
454                },
455                self.validation_context,
456                self.errors,
457            ),
458            Err((name, errors)) => Self::transition(
459                self.config,
460                self.validation_context,
461                merge_errors_vecs(
462                    self.errors,
463                    errors
464                        .into_iter()
465                        .map(|error| ConfigError::Node(name.clone(), error).into())
466                        .collect::<Vec<_>>(),
467                ),
468            ),
469        }
470    }
471
472    /// Add a new full node using a nested [`NodeConfigBuilder`].
473    /// The node will be configured as a full node (non-validator).
474    pub fn with_fullnode(
475        self,
476        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
477    ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
478        match self.create_node_builder(f).validator(false).build() {
479            Ok(node) => Self::transition(
480                RelaychainConfig {
481                    nodes: [self.config.nodes, vec![node]].concat(),
482                    ..self.config
483                },
484                self.validation_context,
485                self.errors,
486            ),
487            Err((name, errors)) => Self::transition(
488                self.config,
489                self.validation_context,
490                merge_errors_vecs(
491                    self.errors,
492                    errors
493                        .into_iter()
494                        .map(|error| ConfigError::Node(name.clone(), error).into())
495                        .collect::<Vec<_>>(),
496                ),
497            ),
498        }
499    }
500
501    /// Add a new node using a nested [`NodeConfigBuilder`].
502    ///
503    /// **Deprecated**: Use [`with_validator`] for validator nodes or [`with_fullnode`] for full nodes instead.
504    #[deprecated(
505        since = "0.4.0",
506        note = "Use `with_validator()` for validator nodes or `with_fullnode()` for full nodes instead"
507    )]
508    pub fn with_node(
509        self,
510        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
511    ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
512        match self.create_node_builder(f).build() {
513            Ok(node) => Self::transition(
514                RelaychainConfig {
515                    nodes: vec![node],
516                    ..self.config
517                },
518                self.validation_context,
519                self.errors,
520            ),
521            Err((name, errors)) => Self::transition(
522                self.config,
523                self.validation_context,
524                merge_errors_vecs(
525                    self.errors,
526                    errors
527                        .into_iter()
528                        .map(|error| ConfigError::Node(name.clone(), error).into())
529                        .collect::<Vec<_>>(),
530                ),
531            ),
532        }
533    }
534
535    /// Add a new group node using a nested [`GroupNodeConfigBuilder`].
536    pub fn with_node_group(
537        self,
538        f: impl FnOnce(GroupNodeConfigBuilder<node::Initial>) -> GroupNodeConfigBuilder<node::Buildable>,
539    ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
540        match f(GroupNodeConfigBuilder::new(
541            self.default_chain_context(),
542            self.validation_context.clone(),
543        ))
544        .build()
545        {
546            Ok(group_node) => Self::transition(
547                RelaychainConfig {
548                    node_groups: vec![group_node],
549                    ..self.config
550                },
551                self.validation_context,
552                self.errors,
553            ),
554            Err((name, errors)) => Self::transition(
555                self.config,
556                self.validation_context,
557                merge_errors_vecs(
558                    self.errors,
559                    errors
560                        .into_iter()
561                        .map(|error| ConfigError::Node(name.clone(), error).into())
562                        .collect::<Vec<_>>(),
563                ),
564            ),
565        }
566    }
567
568    /// Set the location or inline value of a json to override the raw chain-spec.
569    pub fn with_raw_spec_override(self, overrides: impl Into<JsonOverrides>) -> Self {
570        Self::transition(
571            RelaychainConfig {
572                raw_spec_override: Some(overrides.into()),
573                ..self.config
574            },
575            self.validation_context,
576            self.errors,
577        )
578    }
579}
580
581impl RelaychainConfigBuilder<WithAtLeastOneNode> {
582    /// Add a new validator node using a nested [`NodeConfigBuilder`].
583    /// The node will be configured as a validator (authority) with the --validator flag.
584    pub fn with_validator(
585        self,
586        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
587    ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
588        match self.create_node_builder(f).validator(true).build() {
589            Ok(node) => Self::transition(
590                RelaychainConfig {
591                    nodes: [self.config.nodes, vec![node]].concat(),
592                    ..self.config
593                },
594                self.validation_context,
595                self.errors,
596            ),
597            Err((name, errors)) => Self::transition(
598                self.config,
599                self.validation_context,
600                merge_errors_vecs(
601                    self.errors,
602                    errors
603                        .into_iter()
604                        .map(|error| ConfigError::Node(name.clone(), error).into())
605                        .collect::<Vec<_>>(),
606                ),
607            ),
608        }
609    }
610
611    /// Add a new full node using a nested [`NodeConfigBuilder`].
612    /// The node will be configured as a full node (non-validator).
613    pub fn with_fullnode(
614        self,
615        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
616    ) -> Self {
617        match self.create_node_builder(f).validator(false).build() {
618            Ok(node) => Self::transition(
619                RelaychainConfig {
620                    nodes: [self.config.nodes, vec![node]].concat(),
621                    ..self.config
622                },
623                self.validation_context,
624                self.errors,
625            ),
626            Err((name, errors)) => Self::transition(
627                self.config,
628                self.validation_context,
629                merge_errors_vecs(
630                    self.errors,
631                    errors
632                        .into_iter()
633                        .map(|error| ConfigError::Node(name.clone(), error).into())
634                        .collect::<Vec<_>>(),
635                ),
636            ),
637        }
638    }
639
640    /// Add a new node using a nested [`NodeConfigBuilder`].
641    ///
642    /// **Deprecated**: Use [`with_validator`] for validator nodes or [`with_fullnode`] for full nodes instead.
643    #[deprecated(
644        since = "0.4.0",
645        note = "Use `with_validator()` for validator nodes or `with_fullnode()` for full nodes instead"
646    )]
647    pub fn with_node(
648        self,
649        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
650    ) -> Self {
651        match self.create_node_builder(f).build() {
652            Ok(node) => Self::transition(
653                RelaychainConfig {
654                    nodes: [self.config.nodes, vec![node]].concat(),
655                    ..self.config
656                },
657                self.validation_context,
658                self.errors,
659            ),
660            Err((name, errors)) => Self::transition(
661                self.config,
662                self.validation_context,
663                merge_errors_vecs(
664                    self.errors,
665                    errors
666                        .into_iter()
667                        .map(|error| ConfigError::Node(name.clone(), error).into())
668                        .collect::<Vec<_>>(),
669                ),
670            ),
671        }
672    }
673
674    /// Add a new group node using a nested [`GroupNodeConfigBuilder`].
675    pub fn with_node_group(
676        self,
677        f: impl FnOnce(GroupNodeConfigBuilder<node::Initial>) -> GroupNodeConfigBuilder<node::Buildable>,
678    ) -> Self {
679        match f(GroupNodeConfigBuilder::new(
680            self.default_chain_context(),
681            self.validation_context.clone(),
682        ))
683        .build()
684        {
685            Ok(group_node) => Self::transition(
686                RelaychainConfig {
687                    node_groups: [self.config.node_groups, vec![group_node]].concat(),
688                    ..self.config
689                },
690                self.validation_context,
691                self.errors,
692            ),
693            Err((name, errors)) => Self::transition(
694                self.config,
695                self.validation_context,
696                merge_errors_vecs(
697                    self.errors,
698                    errors
699                        .into_iter()
700                        .map(|error| ConfigError::Node(name.clone(), error).into())
701                        .collect::<Vec<_>>(),
702                ),
703            ),
704        }
705    }
706
707    /// Seals the builder and returns a [`RelaychainConfig`] if there are no validation errors, else returns errors.
708    pub fn build(self) -> Result<RelaychainConfig, Vec<anyhow::Error>> {
709        if !self.errors.is_empty() {
710            return Err(self
711                .errors
712                .into_iter()
713                .map(|error| ConfigError::Relaychain(error).into())
714                .collect::<Vec<_>>());
715        }
716
717        Ok(self.config)
718    }
719}
720
721#[cfg(test)]
722mod tests {
723    use super::*;
724
725    #[test]
726    fn relaychain_config_builder_should_succeeds_and_returns_a_relaychain_config() {
727        let relaychain_config = RelaychainConfigBuilder::new(Default::default())
728            .with_chain("polkadot")
729            .with_default_image("myrepo:myimage")
730            .with_default_command("default_command")
731            .with_default_resources(|resources| {
732                resources
733                    .with_limit_cpu("500M")
734                    .with_limit_memory("1G")
735                    .with_request_cpu("250M")
736            })
737            .with_default_db_snapshot("https://www.urltomysnapshot.com/file.tgz")
738            .with_chain_spec_path("./path/to/chain/spec.json")
739            .with_wasm_override("./path/to/override/runtime.wasm")
740            .with_raw_spec_override(serde_json::json!({"some_override_key": "some_override_val"}))
741            .with_default_args(vec![("--arg1", "value1").into(), "--option2".into()])
742            .with_random_nominators_count(42)
743            .with_max_nominations(5)
744            .with_fullnode(|node| node.with_name("node1").bootnode(true))
745            .with_validator(|node| node.with_name("node2").with_command("command2"))
746            .build()
747            .unwrap();
748
749        assert_eq!(relaychain_config.chain().as_str(), "polkadot");
750        assert_eq!(relaychain_config.nodes().len(), 2);
751        let &node1 = relaychain_config.nodes().first().unwrap();
752        assert_eq!(node1.name(), "node1");
753        assert_eq!(node1.command().unwrap().as_str(), "default_command");
754        assert!(node1.is_bootnode());
755        let &node2 = relaychain_config.nodes().last().unwrap();
756        assert_eq!(node2.name(), "node2");
757        assert_eq!(node2.command().unwrap().as_str(), "command2");
758        assert!(node2.is_validator());
759        assert_eq!(
760            relaychain_config.default_command().unwrap().as_str(),
761            "default_command"
762        );
763        assert_eq!(
764            relaychain_config.default_image().unwrap().as_str(),
765            "myrepo:myimage"
766        );
767        let default_resources = relaychain_config.default_resources().unwrap();
768        assert_eq!(default_resources.limit_cpu().unwrap().as_str(), "500M");
769        assert_eq!(default_resources.limit_memory().unwrap().as_str(), "1G");
770        assert_eq!(default_resources.request_cpu().unwrap().as_str(), "250M");
771        assert!(matches!(
772            relaychain_config.default_db_snapshot().unwrap(),
773            AssetLocation::Url(value) if value.as_str() == "https://www.urltomysnapshot.com/file.tgz",
774        ));
775        assert!(matches!(
776            relaychain_config.chain_spec_path().unwrap(),
777            AssetLocation::FilePath(value) if value.to_str().unwrap() == "./path/to/chain/spec.json"
778        ));
779        assert!(matches!(
780            relaychain_config.wasm_override().unwrap(),
781            AssetLocation::FilePath(value) if value.to_str().unwrap() == "./path/to/override/runtime.wasm"
782        ));
783        let args: Vec<Arg> = vec![("--arg1", "value1").into(), "--option2".into()];
784        assert_eq!(
785            relaychain_config.default_args(),
786            args.iter().collect::<Vec<_>>()
787        );
788        assert_eq!(relaychain_config.random_nominators_count().unwrap(), 42);
789        assert_eq!(relaychain_config.max_nominations().unwrap(), 5);
790
791        assert!(matches!(
792            relaychain_config.raw_spec_override().unwrap(),
793            JsonOverrides::Json(value) if *value == serde_json::json!({"some_override_key": "some_override_val"})
794        ));
795    }
796
797    #[test]
798    fn relaychain_config_builder_should_fails_and_returns_an_error_if_chain_is_invalid() {
799        let errors = RelaychainConfigBuilder::new(Default::default())
800            .with_chain("invalid chain")
801            .with_validator(|node| node.with_name("node").with_command("command"))
802            .build()
803            .unwrap_err();
804
805        assert_eq!(errors.len(), 1);
806        assert_eq!(
807            errors.first().unwrap().to_string(),
808            "relaychain.chain: 'invalid chain' shouldn't contains whitespace"
809        );
810    }
811
812    #[test]
813    fn relaychain_config_builder_should_fails_and_returns_an_error_if_default_command_is_invalid() {
814        let errors = RelaychainConfigBuilder::new(Default::default())
815            .with_chain("chain")
816            .with_default_command("invalid command")
817            .with_validator(|node| node.with_name("node").with_command("command"))
818            .build()
819            .unwrap_err();
820
821        assert_eq!(errors.len(), 1);
822        assert_eq!(
823            errors.first().unwrap().to_string(),
824            "relaychain.default_command: 'invalid command' shouldn't contains whitespace"
825        );
826    }
827
828    #[test]
829    fn relaychain_config_builder_should_fails_and_returns_an_error_if_default_image_is_invalid() {
830        let errors = RelaychainConfigBuilder::new(Default::default())
831            .with_chain("chain")
832            .with_default_image("invalid image")
833            .with_validator(|node| node.with_name("node").with_command("command"))
834            .build()
835            .unwrap_err();
836
837        assert_eq!(errors.len(), 1);
838        assert_eq!(
839            errors.first().unwrap().to_string(),
840            r"relaychain.default_image: 'invalid image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
841        );
842    }
843
844    #[test]
845    fn relaychain_config_builder_should_fails_and_returns_an_error_if_default_resources_are_invalid(
846    ) {
847        let errors = RelaychainConfigBuilder::new(Default::default())
848            .with_chain("chain")
849            .with_default_resources(|default_resources| {
850                default_resources
851                    .with_limit_memory("100m")
852                    .with_request_cpu("invalid")
853            })
854            .with_validator(|node| node.with_name("node").with_command("command"))
855            .build()
856            .unwrap_err();
857
858        assert_eq!(errors.len(), 1);
859        assert_eq!(
860            errors.first().unwrap().to_string(),
861            r"relaychain.default_resources.request_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
862        );
863    }
864
865    #[test]
866    fn relaychain_config_builder_should_fails_and_returns_an_error_if_first_node_is_invalid() {
867        let errors = RelaychainConfigBuilder::new(Default::default())
868            .with_chain("chain")
869            .with_validator(|node| node.with_name("node").with_command("invalid command"))
870            .build()
871            .unwrap_err();
872
873        assert_eq!(errors.len(), 1);
874        assert_eq!(
875            errors.first().unwrap().to_string(),
876            "relaychain.nodes['node'].command: 'invalid command' shouldn't contains whitespace"
877        );
878    }
879
880    #[test]
881    fn relaychain_config_builder_with_at_least_one_node_should_fails_and_returns_an_error_if_second_node_is_invalid(
882    ) {
883        let errors = RelaychainConfigBuilder::new(Default::default())
884            .with_chain("chain")
885            .with_validator(|node| node.with_name("node1").with_command("command1"))
886            .with_validator(|node| node.with_name("node2").with_command("invalid command"))
887            .build()
888            .unwrap_err();
889
890        assert_eq!(errors.len(), 1);
891        assert_eq!(
892            errors.first().unwrap().to_string(),
893            "relaychain.nodes['node2'].command: 'invalid command' shouldn't contains whitespace"
894        );
895    }
896
897    #[test]
898    fn relaychain_config_builder_should_fails_returns_multiple_errors_if_a_node_and_default_resources_are_invalid(
899    ) {
900        let errors = RelaychainConfigBuilder::new(Default::default())
901            .with_chain("chain")
902            .with_default_resources(|resources| {
903                resources
904                    .with_request_cpu("100Mi")
905                    .with_limit_memory("1Gi")
906                    .with_limit_cpu("invalid")
907            })
908            .with_validator(|node| node.with_name("node").with_image("invalid image"))
909            .build()
910            .unwrap_err();
911
912        assert_eq!(errors.len(), 2);
913        assert_eq!(
914            errors.first().unwrap().to_string(),
915            "relaychain.default_resources.limit_cpu: 'invalid' doesn't match regex '^\\d+(.\\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
916        );
917        assert_eq!(
918            errors.get(1).unwrap().to_string(),
919            "relaychain.nodes['node'].image: 'invalid image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
920        );
921    }
922
923    #[test]
924    fn relaychain_config_builder_should_works_with_chain_spec_command() {
925        const CMD_TPL: &str = "./bin/chain-spec-generator {% raw %} {{chainName}} {% endraw %}";
926        let config = RelaychainConfigBuilder::new(Default::default())
927            .with_chain("polkadot")
928            .with_default_image("myrepo:myimage")
929            .with_default_command("default_command")
930            .with_chain_spec_command(CMD_TPL)
931            .with_fullnode(|node| node.with_name("node1").bootnode(true))
932            .build()
933            .unwrap();
934
935        assert_eq!(config.chain_spec_command(), Some(CMD_TPL));
936        assert!(!config.chain_spec_command_is_local());
937    }
938
939    #[test]
940    fn relaychain_config_builder_should_works_with_chain_spec_command_locally() {
941        const CMD_TPL: &str = "./bin/chain-spec-generator {% raw %} {{chainName}} {% endraw %}";
942        let config = RelaychainConfigBuilder::new(Default::default())
943            .with_chain("polkadot")
944            .with_default_image("myrepo:myimage")
945            .with_default_command("default_command")
946            .with_chain_spec_command(CMD_TPL)
947            .chain_spec_command_is_local(true)
948            .with_fullnode(|node| node.with_name("node1").bootnode(true))
949            .build()
950            .unwrap();
951
952        assert_eq!(config.chain_spec_command(), Some(CMD_TPL));
953        assert!(config.chain_spec_command_is_local());
954    }
955
956    #[test]
957    fn relaychain_with_group_config_should_succeeds_and_returns_a_relaychain_config() {
958        let relaychain_config = RelaychainConfigBuilder::new(Default::default())
959            .with_chain("chain")
960            .with_default_command("command")
961            .with_validator(|node| node.with_name("node").with_command("node_command"))
962            .with_node_group(|group| {
963                group.with_count(2).with_base_node(|base| {
964                    base.with_name("group_node")
965                        .with_command("some_command")
966                        .with_image("repo:image")
967                        .validator(true)
968                })
969            })
970            .build()
971            .unwrap();
972
973        assert_eq!(relaychain_config.chain().as_str(), "chain");
974        assert_eq!(relaychain_config.nodes().len(), 1);
975        assert_eq!(relaychain_config.group_node_configs().len(), 1);
976        assert_eq!(
977            relaychain_config
978                .group_node_configs()
979                .first()
980                .unwrap()
981                .count,
982            2
983        );
984        let &node = relaychain_config.nodes().first().unwrap();
985        assert_eq!(node.name(), "node");
986        assert_eq!(node.command().unwrap().as_str(), "node_command");
987
988        let group_nodes = relaychain_config.group_node_configs();
989        let group_base_node = group_nodes.first().unwrap();
990        assert_eq!(group_base_node.base_config.name(), "group_node");
991        assert_eq!(
992            group_base_node.base_config.command().unwrap().as_str(),
993            "some_command"
994        );
995        assert_eq!(
996            group_base_node.base_config.image().unwrap().as_str(),
997            "repo:image"
998        );
999        assert!(group_base_node.base_config.is_validator());
1000    }
1001
1002    #[test]
1003    fn relaychain_with_group_count_0_config_should_fail() {
1004        let relaychain_config = RelaychainConfigBuilder::new(Default::default())
1005            .with_chain("chain")
1006            .with_default_command("command")
1007            .with_validator(|node| node.with_name("node").with_command("node_command"))
1008            .with_node_group(|group| {
1009                group.with_count(0).with_base_node(|base| {
1010                    base.with_name("group_node")
1011                        .with_command("some_command")
1012                        .with_image("repo:image")
1013                        .validator(true)
1014                })
1015            })
1016            .build();
1017
1018        let errors: Vec<anyhow::Error> = match relaychain_config {
1019            Ok(_) => vec![],
1020            Err(errs) => errs,
1021        };
1022
1023        assert_eq!(errors.len(), 1);
1024        assert_eq!(
1025            errors.first().unwrap().to_string(),
1026            "relaychain.nodes['group_node'].Count cannot be zero"
1027        );
1028    }
1029}