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#[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 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 wasm_override: Option<AssetLocation>,
50 command: Option<Command>,
51 raw_spec_override: Option<JsonOverrides>,
53}
54
55impl RelaychainConfig {
56 pub fn chain(&self) -> &Chain {
58 &self.chain
59 }
60
61 pub fn default_command(&self) -> Option<&Command> {
63 self.default_command.as_ref()
64 }
65
66 pub fn default_image(&self) -> Option<&Image> {
68 self.default_image.as_ref()
69 }
70
71 pub fn default_resources(&self) -> Option<&Resources> {
73 self.default_resources.as_ref()
74 }
75
76 pub fn default_db_snapshot(&self) -> Option<&AssetLocation> {
78 self.default_db_snapshot.as_ref()
79 }
80
81 pub fn default_args(&self) -> Vec<&Arg> {
83 self.default_args.iter().collect::<Vec<&Arg>>()
84 }
85
86 pub fn chain_spec_path(&self) -> Option<&AssetLocation> {
88 self.chain_spec_path.as_ref()
89 }
90
91 pub fn wasm_override(&self) -> Option<&AssetLocation> {
93 self.wasm_override.as_ref()
94 }
95
96 pub fn chain_spec_command(&self) -> Option<&str> {
98 self.chain_spec_command.as_deref()
99 }
100
101 pub fn chain_spec_command_is_local(&self) -> bool {
103 self.chain_spec_command_is_local
104 }
105
106 pub fn command(&self) -> Option<&Command> {
108 self.command.as_ref()
109 }
110
111 pub fn random_nominators_count(&self) -> Option<u32> {
113 self.random_nominators_count
114 }
115
116 pub fn max_nominations(&self) -> Option<u8> {
118 self.max_nominations
119 }
120
121 pub fn runtime_genesis_patch(&self) -> Option<&serde_json::Value> {
123 self.runtime_genesis_patch.as_ref()
124 }
125
126 pub fn nodes(&self) -> Vec<&NodeConfig> {
128 self.nodes.iter().collect::<Vec<&NodeConfig>>()
129 }
130
131 pub fn group_node_configs(&self) -> Vec<&GroupNodeConfig> {
133 self.node_groups.iter().collect::<Vec<&GroupNodeConfig>>()
134 }
135
136 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
152pub 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, 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 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 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 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 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 #[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 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 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}