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, NodeConfig, NodeConfigBuilder},
12        resources::{Resources, ResourcesBuilder},
13        types::{
14            Arg, AssetLocation, Chain, ChainDefaultContext, Command, Image, ValidationContext,
15        },
16    },
17    utils::{default_command_polkadot, default_relaychain_chain, is_false},
18};
19
20/// A relay chain configuration, composed of nodes and fine-grained configuration options.
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub struct RelaychainConfig {
23    #[serde(default = "default_relaychain_chain")]
24    chain: Chain,
25    #[serde(default = "default_command_polkadot")]
26    default_command: Option<Command>,
27    default_image: Option<Image>,
28    default_resources: Option<Resources>,
29    default_db_snapshot: Option<AssetLocation>,
30    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
31    default_args: Vec<Arg>,
32    chain_spec_path: Option<AssetLocation>,
33    // Full _template_ command, will be rendered (using custom token replacements)
34    // and executed for generate the chain-spec.
35    // available tokens {{chainName}} / {{disableBootnodes}}
36    chain_spec_command: Option<String>,
37    #[serde(skip_serializing_if = "is_false", default)]
38    chain_spec_command_is_local: bool,
39    random_nominators_count: Option<u32>,
40    max_nominations: Option<u8>,
41    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
42    nodes: Vec<NodeConfig>,
43    #[serde(rename = "genesis", skip_serializing_if = "Option::is_none")]
44    runtime_genesis_patch: Option<serde_json::Value>,
45    // Path or url to override the runtime (:code) in the chain-spec
46    wasm_override: Option<AssetLocation>,
47    command: Option<Command>,
48}
49
50impl RelaychainConfig {
51    /// The chain name.
52    pub fn chain(&self) -> &Chain {
53        &self.chain
54    }
55
56    /// The default command used for nodes.
57    pub fn default_command(&self) -> Option<&Command> {
58        self.default_command.as_ref()
59    }
60
61    /// The default container image used for nodes.
62    pub fn default_image(&self) -> Option<&Image> {
63        self.default_image.as_ref()
64    }
65
66    /// The default resources limits used for nodes.
67    pub fn default_resources(&self) -> Option<&Resources> {
68        self.default_resources.as_ref()
69    }
70
71    /// The default database snapshot location that will be used for state.
72    pub fn default_db_snapshot(&self) -> Option<&AssetLocation> {
73        self.default_db_snapshot.as_ref()
74    }
75
76    /// The default arguments that will be used to launch the node command.
77    pub fn default_args(&self) -> Vec<&Arg> {
78        self.default_args.iter().collect::<Vec<&Arg>>()
79    }
80
81    /// The location of an pre-existing chain specification for the relay chain.
82    pub fn chain_spec_path(&self) -> Option<&AssetLocation> {
83        self.chain_spec_path.as_ref()
84    }
85
86    /// The location of a wasm runtime to override in the chain-spec.
87    pub fn wasm_override(&self) -> Option<&AssetLocation> {
88        self.wasm_override.as_ref()
89    }
90
91    /// The full _template_ command to genera the chain-spec
92    pub fn chain_spec_command(&self) -> Option<&str> {
93        self.chain_spec_command.as_deref()
94    }
95
96    /// Does the chain_spec_command needs to be run locally
97    pub fn chain_spec_command_is_local(&self) -> bool {
98        self.chain_spec_command_is_local
99    }
100
101    /// The non-default command used for nodes.
102    pub fn command(&self) -> Option<&Command> {
103        self.command.as_ref()
104    }
105
106    /// 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.
107    pub fn random_nominators_count(&self) -> Option<u32> {
108        self.random_nominators_count
109    }
110
111    /// The maximum number of nominations to create per nominator.
112    pub fn max_nominations(&self) -> Option<u8> {
113        self.max_nominations
114    }
115
116    /// The genesis overrides as a JSON value.
117    pub fn runtime_genesis_patch(&self) -> Option<&serde_json::Value> {
118        self.runtime_genesis_patch.as_ref()
119    }
120
121    /// The nodes of the relay chain.
122    pub fn nodes(&self) -> Vec<&NodeConfig> {
123        self.nodes.iter().collect::<Vec<&NodeConfig>>()
124    }
125
126    pub(crate) fn set_nodes(&mut self, nodes: Vec<NodeConfig>) {
127        self.nodes = nodes;
128    }
129}
130
131states! {
132    Initial,
133    WithChain,
134    WithAtLeastOneNode
135}
136
137/// A relay chain configuration builder, used to build a [`RelaychainConfig`] declaratively with fields validation.
138pub struct RelaychainConfigBuilder<State> {
139    config: RelaychainConfig,
140    validation_context: Rc<RefCell<ValidationContext>>,
141    errors: Vec<anyhow::Error>,
142    _state: PhantomData<State>,
143}
144
145impl Default for RelaychainConfigBuilder<Initial> {
146    fn default() -> Self {
147        Self {
148            config: RelaychainConfig {
149                chain: "default"
150                    .try_into()
151                    .expect(&format!("{DEFAULT_TYPESTATE} {THIS_IS_A_BUG}")),
152                default_command: None,
153                default_image: None,
154                default_resources: None,
155                default_db_snapshot: None,
156                default_args: vec![],
157                chain_spec_path: None,
158                chain_spec_command: None,
159                wasm_override: None,
160                chain_spec_command_is_local: false, // remote cmd by default
161                command: None,
162                random_nominators_count: None,
163                max_nominations: None,
164                runtime_genesis_patch: None,
165                nodes: vec![],
166            },
167            validation_context: Default::default(),
168            errors: vec![],
169            _state: PhantomData,
170        }
171    }
172}
173
174impl<A> RelaychainConfigBuilder<A> {
175    fn transition<B>(
176        config: RelaychainConfig,
177        validation_context: Rc<RefCell<ValidationContext>>,
178        errors: Vec<anyhow::Error>,
179    ) -> RelaychainConfigBuilder<B> {
180        RelaychainConfigBuilder {
181            config,
182            validation_context,
183            errors,
184            _state: PhantomData,
185        }
186    }
187
188    fn default_chain_context(&self) -> ChainDefaultContext {
189        ChainDefaultContext {
190            default_command: self.config.default_command.clone(),
191            default_image: self.config.default_image.clone(),
192            default_resources: self.config.default_resources.clone(),
193            default_db_snapshot: self.config.default_db_snapshot.clone(),
194            default_args: self.config.default_args.clone(),
195        }
196    }
197}
198
199impl RelaychainConfigBuilder<Initial> {
200    pub fn new(
201        validation_context: Rc<RefCell<ValidationContext>>,
202    ) -> RelaychainConfigBuilder<Initial> {
203        Self {
204            validation_context,
205            ..Self::default()
206        }
207    }
208
209    /// Set the chain name (e.g. rococo-local).
210    pub fn with_chain<T>(self, chain: T) -> RelaychainConfigBuilder<WithChain>
211    where
212        T: TryInto<Chain>,
213        T::Error: Error + Send + Sync + 'static,
214    {
215        match chain.try_into() {
216            Ok(chain) => Self::transition(
217                RelaychainConfig {
218                    chain,
219                    ..self.config
220                },
221                self.validation_context,
222                self.errors,
223            ),
224            Err(error) => Self::transition(
225                self.config,
226                self.validation_context,
227                merge_errors(self.errors, FieldError::Chain(error.into()).into()),
228            ),
229        }
230    }
231}
232
233impl RelaychainConfigBuilder<WithChain> {
234    /// Set the default command used for nodes. Can be overridden.
235    pub fn with_default_command<T>(self, command: T) -> Self
236    where
237        T: TryInto<Command>,
238        T::Error: Error + Send + Sync + 'static,
239    {
240        match command.try_into() {
241            Ok(command) => Self::transition(
242                RelaychainConfig {
243                    default_command: Some(command),
244                    ..self.config
245                },
246                self.validation_context,
247                self.errors,
248            ),
249            Err(error) => Self::transition(
250                self.config,
251                self.validation_context,
252                merge_errors(self.errors, FieldError::DefaultCommand(error.into()).into()),
253            ),
254        }
255    }
256
257    /// Set the default container image used for nodes. Can be overridden.
258    pub fn with_default_image<T>(self, image: T) -> Self
259    where
260        T: TryInto<Image>,
261        T::Error: Error + Send + Sync + 'static,
262    {
263        match image.try_into() {
264            Ok(image) => Self::transition(
265                RelaychainConfig {
266                    default_image: Some(image),
267                    ..self.config
268                },
269                self.validation_context,
270                self.errors,
271            ),
272            Err(error) => Self::transition(
273                self.config,
274                self.validation_context,
275                merge_errors(self.errors, FieldError::DefaultImage(error.into()).into()),
276            ),
277        }
278    }
279
280    /// Set the default resources limits used for nodes. Can be overridden.
281    pub fn with_default_resources(
282        self,
283        f: impl FnOnce(ResourcesBuilder) -> ResourcesBuilder,
284    ) -> Self {
285        match f(ResourcesBuilder::new()).build() {
286            Ok(default_resources) => Self::transition(
287                RelaychainConfig {
288                    default_resources: Some(default_resources),
289                    ..self.config
290                },
291                self.validation_context,
292                self.errors,
293            ),
294            Err(errors) => Self::transition(
295                self.config,
296                self.validation_context,
297                merge_errors_vecs(
298                    self.errors,
299                    errors
300                        .into_iter()
301                        .map(|error| FieldError::DefaultResources(error).into())
302                        .collect::<Vec<_>>(),
303                ),
304            ),
305        }
306    }
307
308    /// Set the default database snapshot location that will be used for state. Can be overridden.
309    pub fn with_default_db_snapshot(self, location: impl Into<AssetLocation>) -> Self {
310        Self::transition(
311            RelaychainConfig {
312                default_db_snapshot: Some(location.into()),
313                ..self.config
314            },
315            self.validation_context,
316            self.errors,
317        )
318    }
319
320    /// Set the default arguments that will be used to execute the node command. Can be overridden.
321    pub fn with_default_args(self, args: Vec<Arg>) -> Self {
322        Self::transition(
323            RelaychainConfig {
324                default_args: args,
325                ..self.config
326            },
327            self.validation_context,
328            self.errors,
329        )
330    }
331
332    /// Set the location of a pre-existing chain specification for the relay chain.
333    pub fn with_chain_spec_path(self, location: impl Into<AssetLocation>) -> Self {
334        Self::transition(
335            RelaychainConfig {
336                chain_spec_path: Some(location.into()),
337                ..self.config
338            },
339            self.validation_context,
340            self.errors,
341        )
342    }
343
344    /// Set the location of a wasm to override the chain-spec.
345    pub fn with_wasm_override(self, location: impl Into<AssetLocation>) -> Self {
346        Self::transition(
347            RelaychainConfig {
348                wasm_override: Some(location.into()),
349                ..self.config
350            },
351            self.validation_context,
352            self.errors,
353        )
354    }
355
356    /// Set the chain-spec command _template_ for the relay chain.
357    pub fn with_chain_spec_command(self, cmd_template: impl Into<String>) -> Self {
358        Self::transition(
359            RelaychainConfig {
360                chain_spec_command: Some(cmd_template.into()),
361                ..self.config
362            },
363            self.validation_context,
364            self.errors,
365        )
366    }
367
368    /// Set if the chain-spec command needs to be run locally or not (false by default)
369    pub fn chain_spec_command_is_local(self, choice: bool) -> Self {
370        Self::transition(
371            RelaychainConfig {
372                chain_spec_command_is_local: choice,
373                ..self.config
374            },
375            self.validation_context,
376            self.errors,
377        )
378    }
379
380    /// 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.
381    pub fn with_random_nominators_count(self, random_nominators_count: u32) -> Self {
382        Self::transition(
383            RelaychainConfig {
384                random_nominators_count: Some(random_nominators_count),
385                ..self.config
386            },
387            self.validation_context,
388            self.errors,
389        )
390    }
391
392    /// Set the maximum number of nominations to create per nominator.
393    pub fn with_max_nominations(self, max_nominations: u8) -> Self {
394        Self::transition(
395            RelaychainConfig {
396                max_nominations: Some(max_nominations),
397                ..self.config
398            },
399            self.validation_context,
400            self.errors,
401        )
402    }
403
404    /// Set the genesis overrides as a JSON object.
405    pub fn with_genesis_overrides(self, genesis_overrides: impl Into<serde_json::Value>) -> Self {
406        Self::transition(
407            RelaychainConfig {
408                runtime_genesis_patch: Some(genesis_overrides.into()),
409                ..self.config
410            },
411            self.validation_context,
412            self.errors,
413        )
414    }
415
416    /// Add a new node using a nested [`NodeConfigBuilder`].
417    pub fn with_node(
418        self,
419        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
420    ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
421        match f(NodeConfigBuilder::new(
422            self.default_chain_context(),
423            self.validation_context.clone(),
424        ))
425        .build()
426        {
427            Ok(node) => Self::transition(
428                RelaychainConfig {
429                    nodes: vec![node],
430                    ..self.config
431                },
432                self.validation_context,
433                self.errors,
434            ),
435            Err((name, errors)) => Self::transition(
436                self.config,
437                self.validation_context,
438                merge_errors_vecs(
439                    self.errors,
440                    errors
441                        .into_iter()
442                        .map(|error| ConfigError::Node(name.clone(), error).into())
443                        .collect::<Vec<_>>(),
444                ),
445            ),
446        }
447    }
448}
449
450impl RelaychainConfigBuilder<WithAtLeastOneNode> {
451    /// Add a new node using a nested [`NodeConfigBuilder`].
452    pub fn with_node(
453        self,
454        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
455    ) -> Self {
456        match f(NodeConfigBuilder::new(
457            self.default_chain_context(),
458            self.validation_context.clone(),
459        ))
460        .build()
461        {
462            Ok(node) => Self::transition(
463                RelaychainConfig {
464                    nodes: [self.config.nodes, vec![node]].concat(),
465                    ..self.config
466                },
467                self.validation_context,
468                self.errors,
469            ),
470            Err((name, errors)) => Self::transition(
471                self.config,
472                self.validation_context,
473                merge_errors_vecs(
474                    self.errors,
475                    errors
476                        .into_iter()
477                        .map(|error| ConfigError::Node(name.clone(), error).into())
478                        .collect::<Vec<_>>(),
479                ),
480            ),
481        }
482    }
483
484    /// Seals the builder and returns a [`RelaychainConfig`] if there are no validation errors, else returns errors.
485    pub fn build(self) -> Result<RelaychainConfig, Vec<anyhow::Error>> {
486        if !self.errors.is_empty() {
487            return Err(self
488                .errors
489                .into_iter()
490                .map(|error| ConfigError::Relaychain(error).into())
491                .collect::<Vec<_>>());
492        }
493
494        Ok(self.config)
495    }
496}
497
498#[cfg(test)]
499mod tests {
500    use super::*;
501
502    #[test]
503    fn relaychain_config_builder_should_succeeds_and_returns_a_relaychain_config() {
504        let relaychain_config = RelaychainConfigBuilder::new(Default::default())
505            .with_chain("polkadot")
506            .with_default_image("myrepo:myimage")
507            .with_default_command("default_command")
508            .with_default_resources(|resources| {
509                resources
510                    .with_limit_cpu("500M")
511                    .with_limit_memory("1G")
512                    .with_request_cpu("250M")
513            })
514            .with_default_db_snapshot("https://www.urltomysnapshot.com/file.tgz")
515            .with_chain_spec_path("./path/to/chain/spec.json")
516            .with_wasm_override("./path/to/override/runtime.wasm")
517            .with_default_args(vec![("--arg1", "value1").into(), "--option2".into()])
518            .with_random_nominators_count(42)
519            .with_max_nominations(5)
520            .with_node(|node| node.with_name("node1").bootnode(true))
521            .with_node(|node| {
522                node.with_name("node2")
523                    .with_command("command2")
524                    .validator(true)
525            })
526            .build()
527            .unwrap();
528
529        assert_eq!(relaychain_config.chain().as_str(), "polkadot");
530        assert_eq!(relaychain_config.nodes().len(), 2);
531        let &node1 = relaychain_config.nodes().first().unwrap();
532        assert_eq!(node1.name(), "node1");
533        assert_eq!(node1.command().unwrap().as_str(), "default_command");
534        assert!(node1.is_bootnode());
535        let &node2 = relaychain_config.nodes().last().unwrap();
536        assert_eq!(node2.name(), "node2");
537        assert_eq!(node2.command().unwrap().as_str(), "command2");
538        assert!(node2.is_validator());
539        assert_eq!(
540            relaychain_config.default_command().unwrap().as_str(),
541            "default_command"
542        );
543        assert_eq!(
544            relaychain_config.default_image().unwrap().as_str(),
545            "myrepo:myimage"
546        );
547        let default_resources = relaychain_config.default_resources().unwrap();
548        assert_eq!(default_resources.limit_cpu().unwrap().as_str(), "500M");
549        assert_eq!(default_resources.limit_memory().unwrap().as_str(), "1G");
550        assert_eq!(default_resources.request_cpu().unwrap().as_str(), "250M");
551        assert!(matches!(
552            relaychain_config.default_db_snapshot().unwrap(),
553            AssetLocation::Url(value) if value.as_str() == "https://www.urltomysnapshot.com/file.tgz",
554        ));
555        assert!(matches!(
556            relaychain_config.chain_spec_path().unwrap(),
557            AssetLocation::FilePath(value) if value.to_str().unwrap() == "./path/to/chain/spec.json"
558        ));
559        assert!(matches!(
560            relaychain_config.wasm_override().unwrap(),
561            AssetLocation::FilePath(value) if value.to_str().unwrap() == "./path/to/override/runtime.wasm"
562        ));
563        let args: Vec<Arg> = vec![("--arg1", "value1").into(), "--option2".into()];
564        assert_eq!(
565            relaychain_config.default_args(),
566            args.iter().collect::<Vec<_>>()
567        );
568        assert_eq!(relaychain_config.random_nominators_count().unwrap(), 42);
569        assert_eq!(relaychain_config.max_nominations().unwrap(), 5);
570    }
571
572    #[test]
573    fn relaychain_config_builder_should_fails_and_returns_an_error_if_chain_is_invalid() {
574        let errors = RelaychainConfigBuilder::new(Default::default())
575            .with_chain("invalid chain")
576            .with_node(|node| {
577                node.with_name("node")
578                    .with_command("command")
579                    .validator(true)
580            })
581            .build()
582            .unwrap_err();
583
584        assert_eq!(errors.len(), 1);
585        assert_eq!(
586            errors.first().unwrap().to_string(),
587            "relaychain.chain: 'invalid chain' shouldn't contains whitespace"
588        );
589    }
590
591    #[test]
592    fn relaychain_config_builder_should_fails_and_returns_an_error_if_default_command_is_invalid() {
593        let errors = RelaychainConfigBuilder::new(Default::default())
594            .with_chain("chain")
595            .with_default_command("invalid command")
596            .with_node(|node| {
597                node.with_name("node")
598                    .with_command("command")
599                    .validator(true)
600            })
601            .build()
602            .unwrap_err();
603
604        assert_eq!(errors.len(), 1);
605        assert_eq!(
606            errors.first().unwrap().to_string(),
607            "relaychain.default_command: 'invalid command' shouldn't contains whitespace"
608        );
609    }
610
611    #[test]
612    fn relaychain_config_builder_should_fails_and_returns_an_error_if_default_image_is_invalid() {
613        let errors = RelaychainConfigBuilder::new(Default::default())
614            .with_chain("chain")
615            .with_default_image("invalid image")
616            .with_node(|node| {
617                node.with_name("node")
618                    .with_command("command")
619                    .validator(true)
620            })
621            .build()
622            .unwrap_err();
623
624        assert_eq!(errors.len(), 1);
625        assert_eq!(
626            errors.first().unwrap().to_string(),
627            r"relaychain.default_image: 'invalid image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
628        );
629    }
630
631    #[test]
632    fn relaychain_config_builder_should_fails_and_returns_an_error_if_default_resources_are_invalid(
633    ) {
634        let errors = RelaychainConfigBuilder::new(Default::default())
635            .with_chain("chain")
636            .with_default_resources(|default_resources| {
637                default_resources
638                    .with_limit_memory("100m")
639                    .with_request_cpu("invalid")
640            })
641            .with_node(|node| {
642                node.with_name("node")
643                    .with_command("command")
644                    .validator(true)
645            })
646            .build()
647            .unwrap_err();
648
649        assert_eq!(errors.len(), 1);
650        assert_eq!(
651            errors.first().unwrap().to_string(),
652            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)?$'"
653        );
654    }
655
656    #[test]
657    fn relaychain_config_builder_should_fails_and_returns_an_error_if_first_node_is_invalid() {
658        let errors = RelaychainConfigBuilder::new(Default::default())
659            .with_chain("chain")
660            .with_node(|node| {
661                node.with_name("node")
662                    .with_command("invalid command")
663                    .validator(true)
664            })
665            .build()
666            .unwrap_err();
667
668        assert_eq!(errors.len(), 1);
669        assert_eq!(
670            errors.first().unwrap().to_string(),
671            "relaychain.nodes['node'].command: 'invalid command' shouldn't contains whitespace"
672        );
673    }
674
675    #[test]
676    fn relaychain_config_builder_with_at_least_one_node_should_fails_and_returns_an_error_if_second_node_is_invalid(
677    ) {
678        let errors = RelaychainConfigBuilder::new(Default::default())
679            .with_chain("chain")
680            .with_node(|node| {
681                node.with_name("node1")
682                    .with_command("command1")
683                    .validator(true)
684            })
685            .with_node(|node| {
686                node.with_name("node2")
687                    .with_command("invalid command")
688                    .validator(true)
689            })
690            .build()
691            .unwrap_err();
692
693        assert_eq!(errors.len(), 1);
694        assert_eq!(
695            errors.first().unwrap().to_string(),
696            "relaychain.nodes['node2'].command: 'invalid command' shouldn't contains whitespace"
697        );
698    }
699
700    #[test]
701    fn relaychain_config_builder_should_fails_returns_multiple_errors_if_a_node_and_default_resources_are_invalid(
702    ) {
703        let errors = RelaychainConfigBuilder::new(Default::default())
704            .with_chain("chain")
705            .with_default_resources(|resources| {
706                resources
707                    .with_request_cpu("100Mi")
708                    .with_limit_memory("1Gi")
709                    .with_limit_cpu("invalid")
710            })
711            .with_node(|node| {
712                node.with_name("node")
713                    .with_image("invalid image")
714                    .validator(true)
715            })
716            .build()
717            .unwrap_err();
718
719        assert_eq!(errors.len(), 2);
720        assert_eq!(
721            errors.first().unwrap().to_string(),
722            "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)?$'"
723        );
724        assert_eq!(
725            errors.get(1).unwrap().to_string(),
726            "relaychain.nodes['node'].image: 'invalid image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
727        );
728    }
729
730    #[test]
731    fn relaychain_config_builder_should_works_with_chain_spec_command() {
732        const CMD_TPL: &str = "./bin/chain-spec-generator {% raw %} {{chainName}} {% endraw %}";
733        let config = RelaychainConfigBuilder::new(Default::default())
734            .with_chain("polkadot")
735            .with_default_image("myrepo:myimage")
736            .with_default_command("default_command")
737            .with_chain_spec_command(CMD_TPL)
738            .with_node(|node| node.with_name("node1").bootnode(true))
739            .build()
740            .unwrap();
741
742        assert_eq!(config.chain_spec_command(), Some(CMD_TPL));
743        assert!(!config.chain_spec_command_is_local());
744    }
745
746    #[test]
747    fn relaychain_config_builder_should_works_with_chain_spec_command_locally() {
748        const CMD_TPL: &str = "./bin/chain-spec-generator {% raw %} {{chainName}} {% endraw %}";
749        let config = RelaychainConfigBuilder::new(Default::default())
750            .with_chain("polkadot")
751            .with_default_image("myrepo:myimage")
752            .with_default_command("default_command")
753            .with_chain_spec_command(CMD_TPL)
754            .chain_spec_command_is_local(true)
755            .with_node(|node| node.with_name("node1").bootnode(true))
756            .build()
757            .unwrap();
758
759        assert_eq!(config.chain_spec_command(), Some(CMD_TPL));
760        assert!(config.chain_spec_command_is_local());
761    }
762}