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