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::{ChainSpecRuntime, JsonOverrides},
18 utils::{default_command_polkadot, default_relaychain_chain, is_false},
19};
20
21#[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>,
35 chain_spec_command: Option<String>,
39 chain_spec_runtime: Option<ChainSpecRuntime>,
42 #[serde(skip_serializing_if = "is_false", default)]
43 chain_spec_command_is_local: bool,
44 chain_spec_command_output_path: Option<String>,
45 random_nominators_count: Option<u32>,
46 max_nominations: Option<u8>,
47 #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
48 nodes: Vec<NodeConfig>,
49 #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
50 node_groups: Vec<GroupNodeConfig>,
51 #[serde(rename = "genesis", skip_serializing_if = "Option::is_none")]
52 runtime_genesis_patch: Option<serde_json::Value>,
53 #[serde(skip_serializing_if = "Option::is_none")]
55 genesis_patch: Option<json_patch::Patch>,
56 wasm_override: Option<AssetLocation>,
58 command: Option<Command>,
59 raw_spec_override: Option<JsonOverrides>,
61 #[serde(skip_serializing_if = "Option::is_none")]
63 post_process_script: Option<String>,
64 #[serde(skip_serializing_if = "is_false", default)]
65 override_session_0: bool,
66}
67
68impl RelaychainConfig {
69 pub fn chain(&self) -> &Chain {
71 &self.chain
72 }
73
74 pub fn default_command(&self) -> Option<&Command> {
76 self.default_command.as_ref()
77 }
78
79 pub fn default_image(&self) -> Option<&Image> {
81 self.default_image.as_ref()
82 }
83
84 pub fn default_resources(&self) -> Option<&Resources> {
86 self.default_resources.as_ref()
87 }
88
89 pub fn default_db_snapshot(&self) -> Option<&AssetLocation> {
91 self.default_db_snapshot.as_ref()
92 }
93
94 pub fn default_args(&self) -> Vec<&Arg> {
96 self.default_args.iter().collect::<Vec<&Arg>>()
97 }
98
99 pub fn chain_spec_path(&self) -> Option<&AssetLocation> {
101 self.chain_spec_path.as_ref()
102 }
103
104 pub fn wasm_override(&self) -> Option<&AssetLocation> {
106 self.wasm_override.as_ref()
107 }
108
109 pub fn chain_spec_command(&self) -> Option<&str> {
111 self.chain_spec_command.as_deref()
112 }
113
114 pub fn chain_spec_command_is_local(&self) -> bool {
116 self.chain_spec_command_is_local
117 }
118
119 pub fn override_session_0(&self) -> bool {
120 self.override_session_0
121 }
122
123 pub fn chain_spec_command_output_path(&self) -> Option<&str> {
126 self.chain_spec_command_output_path.as_deref()
127 }
128
129 pub fn command(&self) -> Option<&Command> {
131 self.command.as_ref()
132 }
133
134 pub fn random_nominators_count(&self) -> Option<u32> {
136 self.random_nominators_count
137 }
138
139 pub fn max_nominations(&self) -> Option<u8> {
141 self.max_nominations
142 }
143
144 pub fn runtime_genesis_patch(&self) -> Option<&serde_json::Value> {
146 self.runtime_genesis_patch.as_ref()
147 }
148
149 pub fn genesis_patch(&self) -> Option<&json_patch::Patch> {
151 self.genesis_patch.as_ref()
152 }
153
154 pub fn nodes(&self) -> Vec<&NodeConfig> {
156 self.nodes.iter().collect::<Vec<&NodeConfig>>()
157 }
158
159 pub fn group_node_configs(&self) -> Vec<&GroupNodeConfig> {
161 self.node_groups.iter().collect::<Vec<&GroupNodeConfig>>()
162 }
163
164 pub fn raw_spec_override(&self) -> Option<&JsonOverrides> {
166 self.raw_spec_override.as_ref()
167 }
168
169 pub fn post_process_script(&self) -> Option<&str> {
171 self.post_process_script.as_deref()
172 }
173
174 pub(crate) fn set_nodes(&mut self, nodes: Vec<NodeConfig>) {
176 self.nodes = nodes;
177 }
178
179 pub fn chain_spec_runtime(&self) -> Option<&ChainSpecRuntime> {
181 self.chain_spec_runtime.as_ref()
182 }
183}
184
185states! {
186 Initial,
187 WithChain,
188 WithAtLeastOneNode
189}
190
191pub struct RelaychainConfigBuilder<State> {
193 config: RelaychainConfig,
194 validation_context: Rc<RefCell<ValidationContext>>,
195 errors: Vec<anyhow::Error>,
196 _state: PhantomData<State>,
197}
198
199impl Default for RelaychainConfigBuilder<Initial> {
200 fn default() -> Self {
201 Self {
202 config: RelaychainConfig {
203 chain: "default"
204 .try_into()
205 .expect(&format!("{DEFAULT_TYPESTATE} {THIS_IS_A_BUG}")),
206 default_command: None,
207 default_image: None,
208 default_resources: None,
209 default_db_snapshot: None,
210 default_args: vec![],
211 chain_spec_path: None,
212 chain_spec_command: None,
213 chain_spec_command_output_path: None,
214 chain_spec_runtime: None,
215 wasm_override: None,
216 chain_spec_command_is_local: false, command: None,
218 random_nominators_count: None,
219 max_nominations: None,
220 runtime_genesis_patch: None,
221 genesis_patch: None,
222 nodes: vec![],
223 node_groups: vec![],
224 raw_spec_override: None,
225 post_process_script: None,
226 override_session_0: false,
227 },
228 validation_context: Default::default(),
229 errors: vec![],
230 _state: PhantomData,
231 }
232 }
233}
234
235impl<A> RelaychainConfigBuilder<A> {
236 fn transition<B>(
237 config: RelaychainConfig,
238 validation_context: Rc<RefCell<ValidationContext>>,
239 errors: Vec<anyhow::Error>,
240 ) -> RelaychainConfigBuilder<B> {
241 RelaychainConfigBuilder {
242 config,
243 validation_context,
244 errors,
245 _state: PhantomData,
246 }
247 }
248
249 fn default_chain_context(&self) -> ChainDefaultContext {
250 ChainDefaultContext {
251 default_command: self.config.default_command.clone(),
252 default_image: self.config.default_image.clone(),
253 default_resources: self.config.default_resources.clone(),
254 default_db_snapshot: self.config.default_db_snapshot.clone(),
255 default_args: self.config.default_args.clone(),
256 }
257 }
258
259 fn create_node_builder<F>(&self, f: F) -> NodeConfigBuilder<node::Buildable>
260 where
261 F: FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
262 {
263 f(NodeConfigBuilder::new(
264 self.default_chain_context(),
265 self.validation_context.clone(),
266 ))
267 }
268
269 pub fn with_post_process_script(mut self, script: impl Into<String>) -> Self {
271 self.config.post_process_script = Some(script.into());
272 self
273 }
274}
275
276impl RelaychainConfigBuilder<Initial> {
277 pub fn new(
278 validation_context: Rc<RefCell<ValidationContext>>,
279 ) -> RelaychainConfigBuilder<Initial> {
280 Self {
281 validation_context,
282 ..Self::default()
283 }
284 }
285
286 pub fn with_chain<T>(self, chain: T) -> RelaychainConfigBuilder<WithChain>
288 where
289 T: TryInto<Chain>,
290 T::Error: Error + Send + Sync + 'static,
291 {
292 match chain.try_into() {
293 Ok(chain) => Self::transition(
294 RelaychainConfig {
295 chain,
296 ..self.config
297 },
298 self.validation_context,
299 self.errors,
300 ),
301 Err(error) => Self::transition(
302 self.config,
303 self.validation_context,
304 merge_errors(self.errors, FieldError::Chain(error.into()).into()),
305 ),
306 }
307 }
308}
309
310impl RelaychainConfigBuilder<WithChain> {
311 pub fn with_default_command<T>(self, command: T) -> Self
313 where
314 T: TryInto<Command>,
315 T::Error: Error + Send + Sync + 'static,
316 {
317 match command.try_into() {
318 Ok(command) => Self::transition(
319 RelaychainConfig {
320 default_command: Some(command),
321 ..self.config
322 },
323 self.validation_context,
324 self.errors,
325 ),
326 Err(error) => Self::transition(
327 self.config,
328 self.validation_context,
329 merge_errors(self.errors, FieldError::DefaultCommand(error.into()).into()),
330 ),
331 }
332 }
333
334 pub fn with_default_image<T>(self, image: T) -> Self
336 where
337 T: TryInto<Image>,
338 T::Error: Error + Send + Sync + 'static,
339 {
340 match image.try_into() {
341 Ok(image) => Self::transition(
342 RelaychainConfig {
343 default_image: Some(image),
344 ..self.config
345 },
346 self.validation_context,
347 self.errors,
348 ),
349 Err(error) => Self::transition(
350 self.config,
351 self.validation_context,
352 merge_errors(self.errors, FieldError::DefaultImage(error.into()).into()),
353 ),
354 }
355 }
356
357 pub fn with_default_resources(
359 self,
360 f: impl FnOnce(ResourcesBuilder) -> ResourcesBuilder,
361 ) -> Self {
362 match f(ResourcesBuilder::new()).build() {
363 Ok(default_resources) => Self::transition(
364 RelaychainConfig {
365 default_resources: Some(default_resources),
366 ..self.config
367 },
368 self.validation_context,
369 self.errors,
370 ),
371 Err(errors) => Self::transition(
372 self.config,
373 self.validation_context,
374 merge_errors_vecs(
375 self.errors,
376 errors
377 .into_iter()
378 .map(|error| FieldError::DefaultResources(error).into())
379 .collect::<Vec<_>>(),
380 ),
381 ),
382 }
383 }
384
385 pub fn with_default_db_snapshot(self, location: impl Into<AssetLocation>) -> Self {
387 Self::transition(
388 RelaychainConfig {
389 default_db_snapshot: Some(location.into()),
390 ..self.config
391 },
392 self.validation_context,
393 self.errors,
394 )
395 }
396
397 pub fn with_optional_default_db_snapshot(
401 self,
402 location: Option<impl Into<AssetLocation>>,
403 ) -> Self {
404 match location {
405 Some(location) => self.with_default_db_snapshot(location),
406 None => self,
407 }
408 }
409
410 pub fn with_default_args(self, args: Vec<Arg>) -> Self {
412 Self::transition(
413 RelaychainConfig {
414 default_args: args,
415 ..self.config
416 },
417 self.validation_context,
418 self.errors,
419 )
420 }
421
422 pub fn with_chain_spec_path(self, location: impl Into<AssetLocation>) -> Self {
424 Self::transition(
425 RelaychainConfig {
426 chain_spec_path: Some(location.into()),
427 ..self.config
428 },
429 self.validation_context,
430 self.errors,
431 )
432 }
433
434 pub fn with_wasm_override(self, location: impl Into<AssetLocation>) -> Self {
436 Self::transition(
437 RelaychainConfig {
438 wasm_override: Some(location.into()),
439 ..self.config
440 },
441 self.validation_context,
442 self.errors,
443 )
444 }
445
446 pub fn with_chain_spec_command(self, cmd_template: impl Into<String>) -> Self {
448 Self::transition(
449 RelaychainConfig {
450 chain_spec_command: Some(cmd_template.into()),
451 ..self.config
452 },
453 self.validation_context,
454 self.errors,
455 )
456 }
457
458 pub fn with_chain_spec_runtime(
462 self,
463 location: impl Into<AssetLocation>,
464 preset: Option<&str>,
465 ) -> Self {
466 let chain_spec_runtime = if let Some(preset) = preset {
467 ChainSpecRuntime::with_preset(location.into(), preset.to_string())
468 } else {
469 ChainSpecRuntime::new(location.into())
470 };
471 Self::transition(
472 RelaychainConfig {
473 chain_spec_runtime: Some(chain_spec_runtime),
474 ..self.config
475 },
476 self.validation_context,
477 self.errors,
478 )
479 }
480
481 pub fn chain_spec_command_is_local(self, choice: bool) -> Self {
483 Self::transition(
484 RelaychainConfig {
485 chain_spec_command_is_local: choice,
486 ..self.config
487 },
488 self.validation_context,
489 self.errors,
490 )
491 }
492
493 pub fn with_override_session_0(self, choice: bool) -> Self {
496 Self::transition(
497 RelaychainConfig {
498 override_session_0: choice,
499 ..self.config
500 },
501 self.validation_context,
502 self.errors,
503 )
504 }
505
506 pub fn with_chain_spec_command_output_path(self, output_path: &str) -> Self {
508 Self::transition(
509 RelaychainConfig {
510 chain_spec_command_output_path: Some(output_path.to_string()),
511 ..self.config
512 },
513 self.validation_context,
514 self.errors,
515 )
516 }
517
518 pub fn with_random_nominators_count(self, random_nominators_count: u32) -> Self {
520 Self::transition(
521 RelaychainConfig {
522 random_nominators_count: Some(random_nominators_count),
523 ..self.config
524 },
525 self.validation_context,
526 self.errors,
527 )
528 }
529
530 pub fn with_max_nominations(self, max_nominations: u8) -> Self {
532 Self::transition(
533 RelaychainConfig {
534 max_nominations: Some(max_nominations),
535 ..self.config
536 },
537 self.validation_context,
538 self.errors,
539 )
540 }
541
542 pub fn with_genesis_overrides(self, genesis_overrides: impl Into<serde_json::Value>) -> Self {
544 Self::transition(
545 RelaychainConfig {
546 runtime_genesis_patch: Some(genesis_overrides.into()),
547 ..self.config
548 },
549 self.validation_context,
550 self.errors,
551 )
552 }
553
554 pub fn with_genesis_patch(self, patch: impl Into<serde_json::Value>) -> Self {
558 match serde_json::from_value::<json_patch::Patch>(patch.into()) {
559 Ok(patch) => Self::transition(
560 RelaychainConfig {
561 genesis_patch: Some(patch),
562 ..self.config
563 },
564 self.validation_context,
565 self.errors,
566 ),
567 Err(error) => Self::transition(
568 self.config,
569 self.validation_context,
570 merge_errors(self.errors, FieldError::GenesisPatch(error.into()).into()),
571 ),
572 }
573 }
574
575 pub fn with_validator(
578 self,
579 f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
580 ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
581 match self.create_node_builder(f).validator(true).build() {
582 Ok(node) => Self::transition(
583 RelaychainConfig {
584 nodes: [self.config.nodes, vec![node]].concat(),
585 ..self.config
586 },
587 self.validation_context,
588 self.errors,
589 ),
590 Err((name, errors)) => Self::transition(
591 self.config,
592 self.validation_context,
593 merge_errors_vecs(
594 self.errors,
595 errors
596 .into_iter()
597 .map(|error| ConfigError::Node(name.clone(), error).into())
598 .collect::<Vec<_>>(),
599 ),
600 ),
601 }
602 }
603
604 pub fn with_fullnode(
607 self,
608 f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
609 ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
610 match self.create_node_builder(f).validator(false).build() {
611 Ok(node) => Self::transition(
612 RelaychainConfig {
613 nodes: [self.config.nodes, vec![node]].concat(),
614 ..self.config
615 },
616 self.validation_context,
617 self.errors,
618 ),
619 Err((name, errors)) => Self::transition(
620 self.config,
621 self.validation_context,
622 merge_errors_vecs(
623 self.errors,
624 errors
625 .into_iter()
626 .map(|error| ConfigError::Node(name.clone(), error).into())
627 .collect::<Vec<_>>(),
628 ),
629 ),
630 }
631 }
632
633 #[deprecated(
637 since = "0.4.0",
638 note = "Use `with_validator()` for validator nodes or `with_fullnode()` for full nodes instead"
639 )]
640 pub fn with_node(
641 self,
642 f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
643 ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
644 match self.create_node_builder(f).build() {
645 Ok(node) => Self::transition(
646 RelaychainConfig {
647 nodes: vec![node],
648 ..self.config
649 },
650 self.validation_context,
651 self.errors,
652 ),
653 Err((name, errors)) => Self::transition(
654 self.config,
655 self.validation_context,
656 merge_errors_vecs(
657 self.errors,
658 errors
659 .into_iter()
660 .map(|error| ConfigError::Node(name.clone(), error).into())
661 .collect::<Vec<_>>(),
662 ),
663 ),
664 }
665 }
666
667 pub fn with_node_group(
669 self,
670 f: impl FnOnce(GroupNodeConfigBuilder<node::Initial>) -> GroupNodeConfigBuilder<node::Buildable>,
671 ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
672 match f(GroupNodeConfigBuilder::new(
673 self.default_chain_context(),
674 self.validation_context.clone(),
675 ))
676 .build()
677 {
678 Ok(group_node) => Self::transition(
679 RelaychainConfig {
680 node_groups: vec![group_node],
681 ..self.config
682 },
683 self.validation_context,
684 self.errors,
685 ),
686 Err((name, errors)) => Self::transition(
687 self.config,
688 self.validation_context,
689 merge_errors_vecs(
690 self.errors,
691 errors
692 .into_iter()
693 .map(|error| ConfigError::Node(name.clone(), error).into())
694 .collect::<Vec<_>>(),
695 ),
696 ),
697 }
698 }
699
700 pub fn with_raw_spec_override(self, overrides: impl Into<JsonOverrides>) -> Self {
702 Self::transition(
703 RelaychainConfig {
704 raw_spec_override: Some(overrides.into()),
705 ..self.config
706 },
707 self.validation_context,
708 self.errors,
709 )
710 }
711}
712
713impl RelaychainConfigBuilder<WithAtLeastOneNode> {
714 pub fn with_validator(
717 self,
718 f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
719 ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
720 match self.create_node_builder(f).validator(true).build() {
721 Ok(node) => Self::transition(
722 RelaychainConfig {
723 nodes: [self.config.nodes, vec![node]].concat(),
724 ..self.config
725 },
726 self.validation_context,
727 self.errors,
728 ),
729 Err((name, errors)) => Self::transition(
730 self.config,
731 self.validation_context,
732 merge_errors_vecs(
733 self.errors,
734 errors
735 .into_iter()
736 .map(|error| ConfigError::Node(name.clone(), error).into())
737 .collect::<Vec<_>>(),
738 ),
739 ),
740 }
741 }
742
743 pub fn with_fullnode(
746 self,
747 f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
748 ) -> Self {
749 match self.create_node_builder(f).validator(false).build() {
750 Ok(node) => Self::transition(
751 RelaychainConfig {
752 nodes: [self.config.nodes, vec![node]].concat(),
753 ..self.config
754 },
755 self.validation_context,
756 self.errors,
757 ),
758 Err((name, errors)) => Self::transition(
759 self.config,
760 self.validation_context,
761 merge_errors_vecs(
762 self.errors,
763 errors
764 .into_iter()
765 .map(|error| ConfigError::Node(name.clone(), error).into())
766 .collect::<Vec<_>>(),
767 ),
768 ),
769 }
770 }
771
772 #[deprecated(
776 since = "0.4.0",
777 note = "Use `with_validator()` for validator nodes or `with_fullnode()` for full nodes instead"
778 )]
779 pub fn with_node(
780 self,
781 f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
782 ) -> Self {
783 match self.create_node_builder(f).build() {
784 Ok(node) => Self::transition(
785 RelaychainConfig {
786 nodes: [self.config.nodes, vec![node]].concat(),
787 ..self.config
788 },
789 self.validation_context,
790 self.errors,
791 ),
792 Err((name, errors)) => Self::transition(
793 self.config,
794 self.validation_context,
795 merge_errors_vecs(
796 self.errors,
797 errors
798 .into_iter()
799 .map(|error| ConfigError::Node(name.clone(), error).into())
800 .collect::<Vec<_>>(),
801 ),
802 ),
803 }
804 }
805
806 pub fn with_node_group(
808 self,
809 f: impl FnOnce(GroupNodeConfigBuilder<node::Initial>) -> GroupNodeConfigBuilder<node::Buildable>,
810 ) -> Self {
811 match f(GroupNodeConfigBuilder::new(
812 self.default_chain_context(),
813 self.validation_context.clone(),
814 ))
815 .build()
816 {
817 Ok(group_node) => Self::transition(
818 RelaychainConfig {
819 node_groups: [self.config.node_groups, vec![group_node]].concat(),
820 ..self.config
821 },
822 self.validation_context,
823 self.errors,
824 ),
825 Err((name, errors)) => Self::transition(
826 self.config,
827 self.validation_context,
828 merge_errors_vecs(
829 self.errors,
830 errors
831 .into_iter()
832 .map(|error| ConfigError::Node(name.clone(), error).into())
833 .collect::<Vec<_>>(),
834 ),
835 ),
836 }
837 }
838
839 pub fn build(self) -> Result<RelaychainConfig, Vec<anyhow::Error>> {
841 if !self.errors.is_empty() {
842 return Err(self
843 .errors
844 .into_iter()
845 .map(|error| ConfigError::Relaychain(error).into())
846 .collect::<Vec<_>>());
847 }
848
849 Ok(self.config)
850 }
851}
852
853#[cfg(test)]
854mod tests {
855 use super::*;
856
857 #[test]
858 fn relaychain_config_builder_should_succeeds_and_returns_a_relaychain_config() {
859 let relaychain_config = RelaychainConfigBuilder::new(Default::default())
860 .with_chain("polkadot")
861 .with_default_image("myrepo:myimage")
862 .with_default_command("default_command")
863 .with_default_resources(|resources| {
864 resources
865 .with_limit_cpu("500M")
866 .with_limit_memory("1G")
867 .with_request_cpu("250M")
868 })
869 .with_default_db_snapshot("https://www.urltomysnapshot.com/file.tgz")
870 .with_chain_spec_path("./path/to/chain/spec.json")
871 .with_chain_spec_runtime("./path/to/runtime.wasm", Some("local_testnet"))
872 .with_wasm_override("./path/to/override/runtime.wasm")
873 .with_raw_spec_override(serde_json::json!({"some_override_key": "some_override_val"}))
874 .with_default_args(vec![("--arg1", "value1").into(), "--option2".into()])
875 .with_random_nominators_count(42)
876 .with_max_nominations(5)
877 .with_fullnode(|node| node.with_name("node1").bootnode(true))
878 .with_validator(|node| node.with_name("node2").with_command("command2"))
879 .build()
880 .unwrap();
881
882 assert_eq!(relaychain_config.chain().as_str(), "polkadot");
883 assert_eq!(relaychain_config.nodes().len(), 2);
884 let &node1 = relaychain_config.nodes().first().unwrap();
885 assert_eq!(node1.name(), "node1");
886 assert_eq!(node1.command().unwrap().as_str(), "default_command");
887 assert!(node1.is_bootnode());
888 let &node2 = relaychain_config.nodes().last().unwrap();
889 assert_eq!(node2.name(), "node2");
890 assert_eq!(node2.command().unwrap().as_str(), "command2");
891 assert!(node2.is_validator());
892 assert_eq!(
893 relaychain_config.default_command().unwrap().as_str(),
894 "default_command"
895 );
896 assert_eq!(
897 relaychain_config.default_image().unwrap().as_str(),
898 "myrepo:myimage"
899 );
900 let default_resources = relaychain_config.default_resources().unwrap();
901 assert_eq!(default_resources.limit_cpu().unwrap().as_str(), "500M");
902 assert_eq!(default_resources.limit_memory().unwrap().as_str(), "1G");
903 assert_eq!(default_resources.request_cpu().unwrap().as_str(), "250M");
904 assert!(matches!(
905 relaychain_config.default_db_snapshot().unwrap(),
906 AssetLocation::Url(value) if value.as_str() == "https://www.urltomysnapshot.com/file.tgz",
907 ));
908 assert!(matches!(
909 relaychain_config.chain_spec_path().unwrap(),
910 AssetLocation::FilePath(value) if value.to_str().unwrap() == "./path/to/chain/spec.json"
911 ));
912 assert!(matches!(
913 &relaychain_config.chain_spec_runtime().unwrap().location,
914 AssetLocation::FilePath(value) if value.to_str().unwrap() == "./path/to/runtime.wasm"
915 ));
916 assert_eq!(
917 relaychain_config
918 .chain_spec_runtime()
919 .unwrap()
920 .preset
921 .as_deref(),
922 Some("local_testnet")
923 );
924 assert!(matches!(
925 relaychain_config.wasm_override().unwrap(),
926 AssetLocation::FilePath(value) if value.to_str().unwrap() == "./path/to/override/runtime.wasm"
927 ));
928 let args: Vec<Arg> = vec![("--arg1", "value1").into(), "--option2".into()];
929 assert_eq!(
930 relaychain_config.default_args(),
931 args.iter().collect::<Vec<_>>()
932 );
933 assert_eq!(relaychain_config.random_nominators_count().unwrap(), 42);
934 assert_eq!(relaychain_config.max_nominations().unwrap(), 5);
935
936 assert!(matches!(
937 relaychain_config.raw_spec_override().unwrap(),
938 JsonOverrides::Json(value) if *value == serde_json::json!({"some_override_key": "some_override_val"})
939 ));
940 }
941
942 #[test]
943 fn with_optional_default_db_snapshot_applies_when_some() {
944 let config = RelaychainConfigBuilder::new(Default::default())
945 .with_chain("polkadot")
946 .with_default_command("cmd")
947 .with_optional_default_db_snapshot(Some("https://example.com/snap.tgz"))
948 .with_validator(|node| node.with_name("alice"))
949 .build()
950 .unwrap();
951 assert!(matches!(
952 config.default_db_snapshot().unwrap(),
953 AssetLocation::Url(value) if value.as_str() == "https://example.com/snap.tgz"
954 ));
955 }
956
957 #[test]
958 fn with_optional_default_db_snapshot_is_noop_when_none() {
959 let config = RelaychainConfigBuilder::new(Default::default())
960 .with_chain("polkadot")
961 .with_default_command("cmd")
962 .with_optional_default_db_snapshot(None::<&str>)
963 .with_validator(|node| node.with_name("alice"))
964 .build()
965 .unwrap();
966 assert!(config.default_db_snapshot().is_none());
967 }
968
969 #[test]
970 fn genesis_patch_is_kept_when_valid_and_reported_when_not() {
971 let patch =
972 serde_json::json!([{"op": "add", "path": "/balances/balances/-", "value": ["5A", 1]}]);
973 let config = RelaychainConfigBuilder::new(Default::default())
974 .with_chain("polkadot")
975 .with_genesis_patch(patch.clone())
976 .with_validator(|node| node.with_name("node").with_command("command"))
977 .build()
978 .unwrap();
979 assert_eq!(
980 serde_json::to_value(config.genesis_patch().unwrap()).unwrap(),
981 patch
982 );
983
984 let errors = RelaychainConfigBuilder::new(Default::default())
985 .with_chain("polkadot")
986 .with_genesis_patch(serde_json::json!([{"op": "append", "path": "/x", "value": 1}]))
987 .with_validator(|node| node.with_name("node").with_command("command"))
988 .build()
989 .unwrap_err();
990 assert_eq!(errors.len(), 1);
991 let error = errors.first().unwrap().to_string();
992 assert!(
993 error.starts_with("relaychain.genesis_patch: unknown variant `append`"),
994 "{error}"
995 );
996 }
997
998 #[test]
999 fn relaychain_config_builder_should_fails_and_returns_an_error_if_chain_is_invalid() {
1000 let errors = RelaychainConfigBuilder::new(Default::default())
1001 .with_chain("invalid chain")
1002 .with_validator(|node| node.with_name("node").with_command("command"))
1003 .build()
1004 .unwrap_err();
1005
1006 assert_eq!(errors.len(), 1);
1007 assert_eq!(
1008 errors.first().unwrap().to_string(),
1009 "relaychain.chain: 'invalid chain' shouldn't contains whitespace"
1010 );
1011 }
1012
1013 #[test]
1014 fn relaychain_config_builder_should_fails_and_returns_an_error_if_default_command_is_invalid() {
1015 let errors = RelaychainConfigBuilder::new(Default::default())
1016 .with_chain("chain")
1017 .with_default_command("invalid command")
1018 .with_validator(|node| node.with_name("node").with_command("command"))
1019 .build()
1020 .unwrap_err();
1021
1022 assert_eq!(errors.len(), 1);
1023 assert_eq!(
1024 errors.first().unwrap().to_string(),
1025 "relaychain.default_command: 'invalid command' shouldn't contains whitespace"
1026 );
1027 }
1028
1029 #[test]
1030 fn relaychain_config_builder_should_fails_and_returns_an_error_if_default_image_is_invalid() {
1031 let errors = RelaychainConfigBuilder::new(Default::default())
1032 .with_chain("chain")
1033 .with_default_image("invalid image")
1034 .with_validator(|node| node.with_name("node").with_command("command"))
1035 .build()
1036 .unwrap_err();
1037
1038 assert_eq!(errors.len(), 1);
1039 assert_eq!(
1040 errors.first().unwrap().to_string(),
1041 r"relaychain.default_image: 'invalid image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1042 );
1043 }
1044
1045 #[test]
1046 fn relaychain_config_builder_should_fails_and_returns_an_error_if_default_resources_are_invalid(
1047 ) {
1048 let errors = RelaychainConfigBuilder::new(Default::default())
1049 .with_chain("chain")
1050 .with_default_resources(|default_resources| {
1051 default_resources
1052 .with_limit_memory("100m")
1053 .with_request_cpu("invalid")
1054 })
1055 .with_validator(|node| node.with_name("node").with_command("command"))
1056 .build()
1057 .unwrap_err();
1058
1059 assert_eq!(errors.len(), 1);
1060 assert_eq!(
1061 errors.first().unwrap().to_string(),
1062 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)?$'"
1063 );
1064 }
1065
1066 #[test]
1067 fn relaychain_config_builder_should_fails_and_returns_an_error_if_first_node_is_invalid() {
1068 let errors = RelaychainConfigBuilder::new(Default::default())
1069 .with_chain("chain")
1070 .with_validator(|node| node.with_name("node").with_command("invalid command"))
1071 .build()
1072 .unwrap_err();
1073
1074 assert_eq!(errors.len(), 1);
1075 assert_eq!(
1076 errors.first().unwrap().to_string(),
1077 "relaychain.nodes['node'].command: 'invalid command' shouldn't contains whitespace"
1078 );
1079 }
1080
1081 #[test]
1082 fn relaychain_config_builder_with_at_least_one_node_should_fails_and_returns_an_error_if_second_node_is_invalid(
1083 ) {
1084 let errors = RelaychainConfigBuilder::new(Default::default())
1085 .with_chain("chain")
1086 .with_validator(|node| node.with_name("node1").with_command("command1"))
1087 .with_validator(|node| node.with_name("node2").with_command("invalid command"))
1088 .build()
1089 .unwrap_err();
1090
1091 assert_eq!(errors.len(), 1);
1092 assert_eq!(
1093 errors.first().unwrap().to_string(),
1094 "relaychain.nodes['node2'].command: 'invalid command' shouldn't contains whitespace"
1095 );
1096 }
1097
1098 #[test]
1099 fn relaychain_config_builder_should_fails_returns_multiple_errors_if_a_node_and_default_resources_are_invalid(
1100 ) {
1101 let errors = RelaychainConfigBuilder::new(Default::default())
1102 .with_chain("chain")
1103 .with_default_resources(|resources| {
1104 resources
1105 .with_request_cpu("100Mi")
1106 .with_limit_memory("1Gi")
1107 .with_limit_cpu("invalid")
1108 })
1109 .with_validator(|node| node.with_name("node").with_image("invalid image"))
1110 .build()
1111 .unwrap_err();
1112
1113 assert_eq!(errors.len(), 2);
1114 assert_eq!(
1115 errors.first().unwrap().to_string(),
1116 "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)?$'"
1117 );
1118 assert_eq!(
1119 errors.get(1).unwrap().to_string(),
1120 "relaychain.nodes['node'].image: 'invalid image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1121 );
1122 }
1123
1124 #[test]
1125 fn relaychain_config_builder_should_works_with_chain_spec_command() {
1126 const CMD_TPL: &str = "./bin/chain-spec-generator {% raw %} {{chainName}} {% endraw %}";
1127 let config = RelaychainConfigBuilder::new(Default::default())
1128 .with_chain("polkadot")
1129 .with_default_image("myrepo:myimage")
1130 .with_default_command("default_command")
1131 .with_chain_spec_command(CMD_TPL)
1132 .with_fullnode(|node| node.with_name("node1").bootnode(true))
1133 .build()
1134 .unwrap();
1135
1136 assert_eq!(config.chain_spec_command(), Some(CMD_TPL));
1137 assert!(!config.chain_spec_command_is_local());
1138 }
1139
1140 #[test]
1141 fn relaychain_config_builder_should_works_with_chain_spec_command_locally() {
1142 const CMD_TPL: &str = "./bin/chain-spec-generator {% raw %} {{chainName}} {% endraw %}";
1143 let config = RelaychainConfigBuilder::new(Default::default())
1144 .with_chain("polkadot")
1145 .with_default_image("myrepo:myimage")
1146 .with_default_command("default_command")
1147 .with_chain_spec_command(CMD_TPL)
1148 .chain_spec_command_is_local(true)
1149 .with_fullnode(|node| node.with_name("node1").bootnode(true))
1150 .build()
1151 .unwrap();
1152
1153 assert_eq!(config.chain_spec_command(), Some(CMD_TPL));
1154 assert!(config.chain_spec_command_is_local());
1155 }
1156
1157 #[test]
1158 fn relaychain_with_group_config_should_succeeds_and_returns_a_relaychain_config() {
1159 let relaychain_config = RelaychainConfigBuilder::new(Default::default())
1160 .with_chain("chain")
1161 .with_default_command("command")
1162 .with_validator(|node| node.with_name("node").with_command("node_command"))
1163 .with_node_group(|group| {
1164 group.with_count(2).with_base_node(|base| {
1165 base.with_name("group_node")
1166 .with_command("some_command")
1167 .with_image("repo:image")
1168 .validator(true)
1169 })
1170 })
1171 .build()
1172 .unwrap();
1173
1174 assert_eq!(relaychain_config.chain().as_str(), "chain");
1175 assert_eq!(relaychain_config.nodes().len(), 1);
1176 assert_eq!(relaychain_config.group_node_configs().len(), 1);
1177 assert_eq!(
1178 relaychain_config
1179 .group_node_configs()
1180 .first()
1181 .unwrap()
1182 .count,
1183 2
1184 );
1185 let &node = relaychain_config.nodes().first().unwrap();
1186 assert_eq!(node.name(), "node");
1187 assert_eq!(node.command().unwrap().as_str(), "node_command");
1188
1189 let group_nodes = relaychain_config.group_node_configs();
1190 let group_base_node = group_nodes.first().unwrap();
1191 assert_eq!(group_base_node.base_config.name(), "group-node");
1192 assert_eq!(
1193 group_base_node.base_config.command().unwrap().as_str(),
1194 "some_command"
1195 );
1196 assert_eq!(
1197 group_base_node.base_config.image().unwrap().as_str(),
1198 "repo:image"
1199 );
1200 assert!(group_base_node.base_config.is_validator());
1201 }
1202
1203 #[test]
1204 fn relaychain_with_group_count_0_config_should_fail() {
1205 let relaychain_config = RelaychainConfigBuilder::new(Default::default())
1206 .with_chain("chain")
1207 .with_default_command("command")
1208 .with_validator(|node| node.with_name("node").with_command("node_command"))
1209 .with_node_group(|group| {
1210 group.with_count(0).with_base_node(|base| {
1211 base.with_name("group_node")
1212 .with_command("some_command")
1213 .with_image("repo:image")
1214 .validator(true)
1215 })
1216 })
1217 .build();
1218
1219 let errors: Vec<anyhow::Error> = match relaychain_config {
1220 Ok(_) => vec![],
1221 Err(errs) => errs,
1222 };
1223
1224 assert_eq!(errors.len(), 1);
1225 assert_eq!(
1226 errors.first().unwrap().to_string(),
1227 "relaychain.nodes['group-node'].Count cannot be zero"
1228 );
1229 }
1230}