feat: Vendor pezkuwi-subxt and pezkuwi-zombienet-sdk into monorepo
- Add pezkuwi-subxt crates to vendor/pezkuwi-subxt - Add pezkuwi-zombienet-sdk crates to vendor/pezkuwi-zombienet-sdk - Convert git dependencies to path dependencies - Add vendor crates to workspace members - Remove test/example crates from vendor (not needed for SDK) - Fix feature propagation issues detected by zepter - Fix workspace inheritance for internal dependencies - All 606 crates now in workspace - All 6919 internal dependency links verified correct - No git dependencies remaining
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
/Cargo.lock
|
||||
@@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "zombienet-configuration"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
publish = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Zombienet sdk config builder, allow to build a network configuration"
|
||||
keywords = ["zombienet", "configuration", "sdk"]
|
||||
|
||||
[dependencies]
|
||||
regex = { workspace = true }
|
||||
lazy_static = { workspace = true }
|
||||
multiaddr = { workspace = true }
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
thiserror = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
toml = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
tokio = { workspace = true, features = ["fs"] }
|
||||
tracing = { workspace = true }
|
||||
|
||||
# zombienet deps
|
||||
support = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
fmt::Display,
|
||||
net::IpAddr,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use multiaddr::Multiaddr;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
shared::{
|
||||
errors::{ConfigError, FieldError},
|
||||
helpers::{merge_errors, merge_errors_vecs},
|
||||
types::Duration,
|
||||
},
|
||||
utils::{default_as_true, default_node_spawn_timeout, default_timeout},
|
||||
};
|
||||
|
||||
/// Global settings applied to an entire network.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GlobalSettings {
|
||||
/// Global bootnodes to use (we will then add more)
|
||||
#[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
|
||||
bootnodes_addresses: Vec<Multiaddr>,
|
||||
// TODO: parse both case in zombienet node version to avoid renamed ?
|
||||
/// Global spawn timeout
|
||||
#[serde(rename = "timeout", default = "default_timeout")]
|
||||
network_spawn_timeout: Duration,
|
||||
// TODO: not used yet
|
||||
/// Node spawn timeout
|
||||
#[serde(default = "default_node_spawn_timeout")]
|
||||
node_spawn_timeout: Duration,
|
||||
// TODO: not used yet
|
||||
/// Local ip to use for construct the direct links
|
||||
local_ip: Option<IpAddr>,
|
||||
/// Directory to use as base dir
|
||||
/// Used to reuse the same files (database) from a previous run,
|
||||
/// also note that we will override the content of some of those files.
|
||||
base_dir: Option<PathBuf>,
|
||||
/// Number of concurrent spawning process to launch, None means try to spawn all at the same time.
|
||||
spawn_concurrency: Option<usize>,
|
||||
/// If enabled, will launch a task to monitor nodes' liveness and tear down the network if there are any.
|
||||
#[serde(default = "default_as_true")]
|
||||
tear_down_on_failure: bool,
|
||||
}
|
||||
|
||||
impl GlobalSettings {
|
||||
/// External bootnode address.
|
||||
pub fn bootnodes_addresses(&self) -> Vec<&Multiaddr> {
|
||||
self.bootnodes_addresses.iter().collect()
|
||||
}
|
||||
|
||||
/// Global spawn timeout in seconds.
|
||||
pub fn network_spawn_timeout(&self) -> Duration {
|
||||
self.network_spawn_timeout
|
||||
}
|
||||
|
||||
/// Individual node spawn timeout in seconds.
|
||||
pub fn node_spawn_timeout(&self) -> Duration {
|
||||
self.node_spawn_timeout
|
||||
}
|
||||
|
||||
/// Local IP used to expose local services (including RPC, metrics and monitoring).
|
||||
pub fn local_ip(&self) -> Option<&IpAddr> {
|
||||
self.local_ip.as_ref()
|
||||
}
|
||||
|
||||
/// Base directory to use (instead a random tmp one)
|
||||
/// All the artifacts will be created in this directory.
|
||||
pub fn base_dir(&self) -> Option<&Path> {
|
||||
self.base_dir.as_deref()
|
||||
}
|
||||
|
||||
/// Number of concurrent spawning process to launch
|
||||
pub fn spawn_concurrency(&self) -> Option<usize> {
|
||||
self.spawn_concurrency
|
||||
}
|
||||
|
||||
/// A flag to tear down the network if there are any unresponsive nodes detected.
|
||||
pub fn tear_down_on_failure(&self) -> bool {
|
||||
self.tear_down_on_failure
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GlobalSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
bootnodes_addresses: Default::default(),
|
||||
network_spawn_timeout: default_timeout(),
|
||||
node_spawn_timeout: default_node_spawn_timeout(),
|
||||
local_ip: Default::default(),
|
||||
base_dir: Default::default(),
|
||||
spawn_concurrency: Default::default(),
|
||||
tear_down_on_failure: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A global settings builder, used to build [`GlobalSettings`] declaratively with fields validation.
|
||||
#[derive(Default)]
|
||||
pub struct GlobalSettingsBuilder {
|
||||
config: GlobalSettings,
|
||||
errors: Vec<anyhow::Error>,
|
||||
}
|
||||
|
||||
impl GlobalSettingsBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
// Transition to the next state of the builder.
|
||||
fn transition(config: GlobalSettings, errors: Vec<anyhow::Error>) -> Self {
|
||||
Self { config, errors }
|
||||
}
|
||||
|
||||
/// Set the external bootnode address.
|
||||
///
|
||||
/// Note: Bootnode address replacements are NOT supported here.
|
||||
/// Only arguments (`args`) support dynamic replacements. Bootnode addresses must be a valid address.
|
||||
pub fn with_raw_bootnodes_addresses<T>(self, bootnodes_addresses: Vec<T>) -> Self
|
||||
where
|
||||
T: TryInto<Multiaddr> + Display + Copy,
|
||||
T::Error: Error + Send + Sync + 'static,
|
||||
{
|
||||
let mut addrs = vec![];
|
||||
let mut errors = vec![];
|
||||
|
||||
for (index, addr) in bootnodes_addresses.into_iter().enumerate() {
|
||||
match addr.try_into() {
|
||||
Ok(addr) => addrs.push(addr),
|
||||
Err(error) => errors.push(
|
||||
FieldError::BootnodesAddress(index, addr.to_string(), error.into()).into(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
Self::transition(
|
||||
GlobalSettings {
|
||||
bootnodes_addresses: addrs,
|
||||
..self.config
|
||||
},
|
||||
merge_errors_vecs(self.errors, errors),
|
||||
)
|
||||
}
|
||||
|
||||
/// Set global spawn timeout in seconds.
|
||||
pub fn with_network_spawn_timeout(self, timeout: Duration) -> Self {
|
||||
Self::transition(
|
||||
GlobalSettings {
|
||||
network_spawn_timeout: timeout,
|
||||
..self.config
|
||||
},
|
||||
self.errors,
|
||||
)
|
||||
}
|
||||
|
||||
/// Set individual node spawn timeout in seconds.
|
||||
pub fn with_node_spawn_timeout(self, timeout: Duration) -> Self {
|
||||
Self::transition(
|
||||
GlobalSettings {
|
||||
node_spawn_timeout: timeout,
|
||||
..self.config
|
||||
},
|
||||
self.errors,
|
||||
)
|
||||
}
|
||||
|
||||
/// Set local IP used to expose local services (including RPC, metrics and monitoring).
|
||||
pub fn with_local_ip(self, local_ip: &str) -> Self {
|
||||
match IpAddr::from_str(local_ip) {
|
||||
Ok(local_ip) => Self::transition(
|
||||
GlobalSettings {
|
||||
local_ip: Some(local_ip),
|
||||
..self.config
|
||||
},
|
||||
self.errors,
|
||||
),
|
||||
Err(error) => Self::transition(
|
||||
self.config,
|
||||
merge_errors(self.errors, FieldError::LocalIp(error.into()).into()),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the directory to use as base (instead of a random tmp one).
|
||||
pub fn with_base_dir(self, base_dir: impl Into<PathBuf>) -> Self {
|
||||
Self::transition(
|
||||
GlobalSettings {
|
||||
base_dir: Some(base_dir.into()),
|
||||
..self.config
|
||||
},
|
||||
self.errors,
|
||||
)
|
||||
}
|
||||
|
||||
/// Set the spawn concurrency
|
||||
pub fn with_spawn_concurrency(self, spawn_concurrency: usize) -> Self {
|
||||
Self::transition(
|
||||
GlobalSettings {
|
||||
spawn_concurrency: Some(spawn_concurrency),
|
||||
..self.config
|
||||
},
|
||||
self.errors,
|
||||
)
|
||||
}
|
||||
|
||||
/// Set the `tear_down_on_failure` flag
|
||||
pub fn with_tear_down_on_failure(self, tear_down_on_failure: bool) -> Self {
|
||||
Self::transition(
|
||||
GlobalSettings {
|
||||
tear_down_on_failure,
|
||||
..self.config
|
||||
},
|
||||
self.errors,
|
||||
)
|
||||
}
|
||||
|
||||
/// Seals the builder and returns a [`GlobalSettings`] if there are no validation errors, else returns errors.
|
||||
pub fn build(self) -> Result<GlobalSettings, Vec<anyhow::Error>> {
|
||||
if !self.errors.is_empty() {
|
||||
return Err(self
|
||||
.errors
|
||||
.into_iter()
|
||||
.map(|error| ConfigError::GlobalSettings(error).into())
|
||||
.collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
Ok(self.config)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn global_settings_config_builder_should_succeeds_and_returns_a_global_settings_config() {
|
||||
let global_settings_config = GlobalSettingsBuilder::new()
|
||||
.with_raw_bootnodes_addresses(vec![
|
||||
"/ip4/10.41.122.55/tcp/45421",
|
||||
"/ip4/51.144.222.10/tcp/2333",
|
||||
])
|
||||
.with_network_spawn_timeout(600)
|
||||
.with_node_spawn_timeout(120)
|
||||
.with_local_ip("10.0.0.1")
|
||||
.with_base_dir("/home/nonroot/mynetwork")
|
||||
.with_spawn_concurrency(5)
|
||||
.with_tear_down_on_failure(true)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let bootnodes_addresses: Vec<Multiaddr> = vec![
|
||||
"/ip4/10.41.122.55/tcp/45421".try_into().unwrap(),
|
||||
"/ip4/51.144.222.10/tcp/2333".try_into().unwrap(),
|
||||
];
|
||||
assert_eq!(
|
||||
global_settings_config.bootnodes_addresses(),
|
||||
bootnodes_addresses.iter().collect::<Vec<_>>()
|
||||
);
|
||||
assert_eq!(global_settings_config.network_spawn_timeout(), 600);
|
||||
assert_eq!(global_settings_config.node_spawn_timeout(), 120);
|
||||
assert_eq!(
|
||||
global_settings_config
|
||||
.local_ip()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.as_str(),
|
||||
"10.0.0.1"
|
||||
);
|
||||
assert_eq!(
|
||||
global_settings_config.base_dir().unwrap(),
|
||||
Path::new("/home/nonroot/mynetwork")
|
||||
);
|
||||
assert_eq!(global_settings_config.spawn_concurrency().unwrap(), 5);
|
||||
assert!(global_settings_config.tear_down_on_failure());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn global_settings_config_builder_should_succeeds_when_node_spawn_timeout_is_missing() {
|
||||
let global_settings_config = GlobalSettingsBuilder::new()
|
||||
.with_raw_bootnodes_addresses(vec![
|
||||
"/ip4/10.41.122.55/tcp/45421",
|
||||
"/ip4/51.144.222.10/tcp/2333",
|
||||
])
|
||||
.with_network_spawn_timeout(600)
|
||||
.with_local_ip("10.0.0.1")
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let bootnodes_addresses: Vec<Multiaddr> = vec![
|
||||
"/ip4/10.41.122.55/tcp/45421".try_into().unwrap(),
|
||||
"/ip4/51.144.222.10/tcp/2333".try_into().unwrap(),
|
||||
];
|
||||
assert_eq!(
|
||||
global_settings_config.bootnodes_addresses(),
|
||||
bootnodes_addresses.iter().collect::<Vec<_>>()
|
||||
);
|
||||
assert_eq!(global_settings_config.network_spawn_timeout(), 600);
|
||||
assert_eq!(global_settings_config.node_spawn_timeout(), 600);
|
||||
assert_eq!(
|
||||
global_settings_config
|
||||
.local_ip()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.as_str(),
|
||||
"10.0.0.1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn global_settings_builder_should_fails_and_returns_an_error_if_one_bootnode_address_is_invalid(
|
||||
) {
|
||||
let errors = GlobalSettingsBuilder::new()
|
||||
.with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421"])
|
||||
.build()
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(errors.len(), 1);
|
||||
assert_eq!(
|
||||
errors.first().unwrap().to_string(),
|
||||
"global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn global_settings_builder_should_fails_and_returns_multiple_errors_if_multiple_bootnodes_addresses_are_invalid(
|
||||
) {
|
||||
let errors = GlobalSettingsBuilder::new()
|
||||
.with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
|
||||
.build()
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(errors.len(), 2);
|
||||
assert_eq!(
|
||||
errors.first().unwrap().to_string(),
|
||||
"global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
|
||||
);
|
||||
assert_eq!(
|
||||
errors.get(1).unwrap().to_string(),
|
||||
"global_settings.bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn global_settings_builder_should_fails_and_returns_an_error_if_local_ip_is_invalid() {
|
||||
let errors = GlobalSettingsBuilder::new()
|
||||
.with_local_ip("invalid")
|
||||
.build()
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(errors.len(), 1);
|
||||
assert_eq!(
|
||||
errors.first().unwrap().to_string(),
|
||||
"global_settings.local_ip: invalid IP address syntax"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn global_settings_builder_should_fails_and_returns_multiple_errors_if_multiple_fields_are_invalid(
|
||||
) {
|
||||
let errors = GlobalSettingsBuilder::new()
|
||||
.with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
|
||||
.with_local_ip("invalid")
|
||||
.build()
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(errors.len(), 3);
|
||||
assert_eq!(
|
||||
errors.first().unwrap().to_string(),
|
||||
"global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
|
||||
);
|
||||
assert_eq!(
|
||||
errors.get(1).unwrap().to_string(),
|
||||
"global_settings.bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
|
||||
);
|
||||
assert_eq!(
|
||||
errors.get(2).unwrap().to_string(),
|
||||
"global_settings.local_ip: invalid IP address syntax"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::shared::{macros::states, types::ParaId};
|
||||
|
||||
/// HRMP channel configuration, with fine-grained configuration options.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct HrmpChannelConfig {
|
||||
sender: ParaId,
|
||||
recipient: ParaId,
|
||||
max_capacity: u32,
|
||||
max_message_size: u32,
|
||||
}
|
||||
|
||||
impl HrmpChannelConfig {
|
||||
/// The sending parachain ID.
|
||||
pub fn sender(&self) -> ParaId {
|
||||
self.sender
|
||||
}
|
||||
|
||||
/// The receiving parachain ID.
|
||||
pub fn recipient(&self) -> ParaId {
|
||||
self.recipient
|
||||
}
|
||||
|
||||
/// The maximum capacity of messages in the channel.
|
||||
pub fn max_capacity(&self) -> u32 {
|
||||
self.max_capacity
|
||||
}
|
||||
|
||||
/// The maximum size of a message in the channel.
|
||||
pub fn max_message_size(&self) -> u32 {
|
||||
self.max_message_size
|
||||
}
|
||||
}
|
||||
|
||||
states! {
|
||||
Initial,
|
||||
WithSender,
|
||||
WithRecipient
|
||||
}
|
||||
|
||||
/// HRMP channel configuration builder, used to build an [`HrmpChannelConfig`] declaratively with fields validation.
|
||||
pub struct HrmpChannelConfigBuilder<State> {
|
||||
config: HrmpChannelConfig,
|
||||
_state: PhantomData<State>,
|
||||
}
|
||||
|
||||
impl Default for HrmpChannelConfigBuilder<Initial> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
config: HrmpChannelConfig {
|
||||
sender: 0,
|
||||
recipient: 0,
|
||||
max_capacity: 8,
|
||||
max_message_size: 512,
|
||||
},
|
||||
_state: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A> HrmpChannelConfigBuilder<A> {
|
||||
fn transition<B>(&self, config: HrmpChannelConfig) -> HrmpChannelConfigBuilder<B> {
|
||||
HrmpChannelConfigBuilder {
|
||||
config,
|
||||
_state: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HrmpChannelConfigBuilder<Initial> {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Set the sending parachain ID.
|
||||
pub fn with_sender(self, sender: ParaId) -> HrmpChannelConfigBuilder<WithSender> {
|
||||
self.transition(HrmpChannelConfig {
|
||||
sender,
|
||||
..self.config
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl HrmpChannelConfigBuilder<WithSender> {
|
||||
/// Set the receiving parachain ID.
|
||||
pub fn with_recipient(self, recipient: ParaId) -> HrmpChannelConfigBuilder<WithRecipient> {
|
||||
self.transition(HrmpChannelConfig {
|
||||
recipient,
|
||||
..self.config
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl HrmpChannelConfigBuilder<WithRecipient> {
|
||||
/// Set the max capacity of messages in the channel.
|
||||
pub fn with_max_capacity(self, max_capacity: u32) -> Self {
|
||||
self.transition(HrmpChannelConfig {
|
||||
max_capacity,
|
||||
..self.config
|
||||
})
|
||||
}
|
||||
|
||||
/// Set the maximum size of a message in the channel.
|
||||
pub fn with_max_message_size(self, max_message_size: u32) -> Self {
|
||||
self.transition(HrmpChannelConfig {
|
||||
max_message_size,
|
||||
..self.config
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build(self) -> HrmpChannelConfig {
|
||||
self.config
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn hrmp_channel_config_builder_should_build_a_new_hrmp_channel_config_correctly() {
|
||||
let hrmp_channel_config = HrmpChannelConfigBuilder::new()
|
||||
.with_sender(1000)
|
||||
.with_recipient(2000)
|
||||
.with_max_capacity(50)
|
||||
.with_max_message_size(100)
|
||||
.build();
|
||||
|
||||
assert_eq!(hrmp_channel_config.sender(), 1000);
|
||||
assert_eq!(hrmp_channel_config.recipient(), 2000);
|
||||
assert_eq!(hrmp_channel_config.max_capacity(), 50);
|
||||
assert_eq!(hrmp_channel_config.max_message_size(), 100);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
//! This crate is used to create type safe configuration for Zombienet SDK using nested builders.
|
||||
//!
|
||||
//!
|
||||
//! The main entry point of this crate is the [`NetworkConfigBuilder`] which is used to build a full network configuration
|
||||
//! but all inner builders are also exposed to allow more granular control over the configuration.
|
||||
//!
|
||||
//! **Note**: Not all options can be checked at compile time and some will be checked at runtime when spawning a
|
||||
//! network (e.g.: supported args for a specific node version).
|
||||
//!
|
||||
//! # Example
|
||||
//! ```
|
||||
//! use zombienet_configuration::NetworkConfigBuilder;
|
||||
//!
|
||||
//! let simple_configuration = NetworkConfigBuilder::new()
|
||||
//! .with_relaychain(|relaychain| {
|
||||
//! relaychain
|
||||
//! .with_chain("polkadot")
|
||||
//! .with_random_nominators_count(10)
|
||||
//! .with_default_resources(|resources| {
|
||||
//! resources
|
||||
//! .with_limit_cpu("1000m")
|
||||
//! .with_request_memory("1Gi")
|
||||
//! .with_request_cpu(100_000)
|
||||
//! })
|
||||
//! .with_node(|node| {
|
||||
//! node.with_name("node")
|
||||
//! .with_command("command")
|
||||
//! .validator(true)
|
||||
//! })
|
||||
//! })
|
||||
//! .with_parachain(|parachain| {
|
||||
//! parachain
|
||||
//! .with_id(1000)
|
||||
//! .with_chain("myparachain1")
|
||||
//! .with_initial_balance(100_000)
|
||||
//! .with_default_image("myimage:version")
|
||||
//! .with_collator(|collator| {
|
||||
//! collator
|
||||
//! .with_name("collator1")
|
||||
//! .with_command("command1")
|
||||
//! .validator(true)
|
||||
//! })
|
||||
//! })
|
||||
//! .with_parachain(|parachain| {
|
||||
//! parachain
|
||||
//! .with_id(2000)
|
||||
//! .with_chain("myparachain2")
|
||||
//! .with_initial_balance(50_0000)
|
||||
//! .with_collator(|collator| {
|
||||
//! collator
|
||||
//! .with_name("collator2")
|
||||
//! .with_command("command2")
|
||||
//! .validator(true)
|
||||
//! })
|
||||
//! })
|
||||
//! .with_hrmp_channel(|hrmp_channel1| {
|
||||
//! hrmp_channel1
|
||||
//! .with_sender(1)
|
||||
//! .with_recipient(2)
|
||||
//! .with_max_capacity(200)
|
||||
//! .with_max_message_size(500)
|
||||
//! })
|
||||
//! .with_hrmp_channel(|hrmp_channel2| {
|
||||
//! hrmp_channel2
|
||||
//! .with_sender(2)
|
||||
//! .with_recipient(1)
|
||||
//! .with_max_capacity(100)
|
||||
//! .with_max_message_size(250)
|
||||
//! })
|
||||
//! .with_global_settings(|global_settings| {
|
||||
//! global_settings
|
||||
//! .with_network_spawn_timeout(1200)
|
||||
//! .with_node_spawn_timeout(240)
|
||||
//! })
|
||||
//! .build();
|
||||
//!
|
||||
//! assert!(simple_configuration.is_ok())
|
||||
//! ```
|
||||
|
||||
#![allow(clippy::expect_fun_call)]
|
||||
mod global_settings;
|
||||
mod hrmp_channel;
|
||||
mod network;
|
||||
mod relaychain;
|
||||
pub mod shared;
|
||||
mod teyrchain;
|
||||
mod utils;
|
||||
|
||||
pub use global_settings::{GlobalSettings, GlobalSettingsBuilder};
|
||||
pub use hrmp_channel::{HrmpChannelConfig, HrmpChannelConfigBuilder};
|
||||
pub use network::{NetworkConfig, NetworkConfigBuilder, WithRelaychain};
|
||||
pub use relaychain::{RelaychainConfig, RelaychainConfigBuilder};
|
||||
// re-export shared
|
||||
pub use shared::{node::NodeConfig, types};
|
||||
pub use teyrchain::{
|
||||
states as para_states, RegistrationStrategy, TeyrchainConfig, TeyrchainConfigBuilder,
|
||||
};
|
||||
|
||||
// Backward compatibility aliases for external crates that use Polkadot SDK terminology
|
||||
// These allow zombienet-orchestrator and other external crates to work with our renamed types
|
||||
pub type ParachainConfig = TeyrchainConfig;
|
||||
pub type ParachainConfigBuilder<S, C> = TeyrchainConfigBuilder<S, C>;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
pub mod errors;
|
||||
pub mod helpers;
|
||||
pub mod macros;
|
||||
pub mod node;
|
||||
pub mod resources;
|
||||
pub mod types;
|
||||
@@ -0,0 +1,116 @@
|
||||
use super::types::{ParaId, Port};
|
||||
|
||||
/// An error at the configuration level.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ConfigError {
|
||||
#[error("relaychain.{0}")]
|
||||
Relaychain(anyhow::Error),
|
||||
|
||||
#[error("teyrchain[{0}].{1}")]
|
||||
Teyrchain(ParaId, anyhow::Error),
|
||||
|
||||
#[error("global_settings.{0}")]
|
||||
GlobalSettings(anyhow::Error),
|
||||
|
||||
#[error("nodes['{0}'].{1}")]
|
||||
Node(String, anyhow::Error),
|
||||
|
||||
#[error("collators['{0}'].{1}")]
|
||||
Collator(String, anyhow::Error),
|
||||
}
|
||||
|
||||
/// An error at the field level.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum FieldError {
|
||||
#[error("name: {0}")]
|
||||
Name(anyhow::Error),
|
||||
|
||||
#[error("chain: {0}")]
|
||||
Chain(anyhow::Error),
|
||||
|
||||
#[error("image: {0}")]
|
||||
Image(anyhow::Error),
|
||||
|
||||
#[error("default_image: {0}")]
|
||||
DefaultImage(anyhow::Error),
|
||||
|
||||
#[error("command: {0}")]
|
||||
Command(anyhow::Error),
|
||||
|
||||
#[error("default_command: {0}")]
|
||||
DefaultCommand(anyhow::Error),
|
||||
|
||||
#[error("bootnodes_addresses[{0}]: '{1}' {2}")]
|
||||
BootnodesAddress(usize, String, anyhow::Error),
|
||||
|
||||
#[error("genesis_wasm_generator: {0}")]
|
||||
GenesisWasmGenerator(anyhow::Error),
|
||||
|
||||
#[error("genesis_state_generator: {0}")]
|
||||
GenesisStateGenerator(anyhow::Error),
|
||||
|
||||
#[error("local_ip: {0}")]
|
||||
LocalIp(anyhow::Error),
|
||||
|
||||
#[error("default_resources.{0}")]
|
||||
DefaultResources(anyhow::Error),
|
||||
|
||||
#[error("resources.{0}")]
|
||||
Resources(anyhow::Error),
|
||||
|
||||
#[error("request_memory: {0}")]
|
||||
RequestMemory(anyhow::Error),
|
||||
|
||||
#[error("request_cpu: {0}")]
|
||||
RequestCpu(anyhow::Error),
|
||||
|
||||
#[error("limit_memory: {0}")]
|
||||
LimitMemory(anyhow::Error),
|
||||
|
||||
#[error("limit_cpu: {0}")]
|
||||
LimitCpu(anyhow::Error),
|
||||
|
||||
#[error("ws_port: {0}")]
|
||||
WsPort(anyhow::Error),
|
||||
|
||||
#[error("rpc_port: {0}")]
|
||||
RpcPort(anyhow::Error),
|
||||
|
||||
#[error("prometheus_port: {0}")]
|
||||
PrometheusPort(anyhow::Error),
|
||||
|
||||
#[error("p2p_port: {0}")]
|
||||
P2pPort(anyhow::Error),
|
||||
|
||||
#[error("session_key: {0}")]
|
||||
SessionKey(anyhow::Error),
|
||||
|
||||
#[error("registration_strategy: {0}")]
|
||||
RegistrationStrategy(anyhow::Error),
|
||||
}
|
||||
|
||||
/// A conversion error for shared types across fields.
|
||||
#[derive(thiserror::Error, Debug, Clone)]
|
||||
pub enum ConversionError {
|
||||
#[error("'{0}' shouldn't contains whitespace")]
|
||||
ContainsWhitespaces(String),
|
||||
|
||||
#[error("'{}' doesn't match regex '{}'", .value, .regex)]
|
||||
DoesntMatchRegex { value: String, regex: String },
|
||||
|
||||
#[error("can't be empty")]
|
||||
CantBeEmpty,
|
||||
|
||||
#[error("deserialize error")]
|
||||
DeserializeError(String),
|
||||
}
|
||||
|
||||
/// A validation error for shared types across fields.
|
||||
#[derive(thiserror::Error, Debug, Clone)]
|
||||
pub enum ValidationError {
|
||||
#[error("'{0}' is already used across config")]
|
||||
PortAlreadyUsed(Port),
|
||||
|
||||
#[error("can't be empty")]
|
||||
CantBeEmpty(),
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
use std::{cell::RefCell, collections::HashSet, rc::Rc};
|
||||
|
||||
use support::constants::{BORROWABLE, THIS_IS_A_BUG};
|
||||
use tracing::warn;
|
||||
|
||||
use super::{
|
||||
errors::ValidationError,
|
||||
types::{ParaId, Port, ValidationContext},
|
||||
};
|
||||
|
||||
pub fn merge_errors(errors: Vec<anyhow::Error>, new_error: anyhow::Error) -> Vec<anyhow::Error> {
|
||||
let mut errors = errors;
|
||||
errors.push(new_error);
|
||||
|
||||
errors
|
||||
}
|
||||
|
||||
pub fn merge_errors_vecs(
|
||||
errors: Vec<anyhow::Error>,
|
||||
new_errors: Vec<anyhow::Error>,
|
||||
) -> Vec<anyhow::Error> {
|
||||
let mut errors = errors;
|
||||
|
||||
for new_error in new_errors.into_iter() {
|
||||
errors.push(new_error);
|
||||
}
|
||||
|
||||
errors
|
||||
}
|
||||
|
||||
/// Generates a unique name from a base name and the names already present in a
|
||||
/// [`ValidationContext`].
|
||||
///
|
||||
/// Uses [`generate_unique_node_name_from_names()`] internally to ensure uniqueness.
|
||||
/// Logs a warning if the generated name differs from the original due to duplicates.
|
||||
pub fn generate_unique_node_name(
|
||||
node_name: impl Into<String>,
|
||||
validation_context: Rc<RefCell<ValidationContext>>,
|
||||
) -> String {
|
||||
let mut context = validation_context
|
||||
.try_borrow_mut()
|
||||
.expect(&format!("{BORROWABLE}, {THIS_IS_A_BUG}"));
|
||||
|
||||
generate_unique_node_name_from_names(node_name, &mut context.used_nodes_names)
|
||||
}
|
||||
|
||||
/// Returns `node_name` if it is not already in `names`.
|
||||
///
|
||||
/// Otherwise, appends an incrementing `-{counter}` suffix until a unique name is found,
|
||||
/// then returns it. Logs a warning when a duplicate is detected.
|
||||
pub fn generate_unique_node_name_from_names(
|
||||
node_name: impl Into<String>,
|
||||
names: &mut HashSet<String>,
|
||||
) -> String {
|
||||
let node_name = node_name.into();
|
||||
|
||||
if names.insert(node_name.clone()) {
|
||||
return node_name;
|
||||
}
|
||||
|
||||
let mut counter = 1;
|
||||
let mut candidate = node_name.clone();
|
||||
while names.contains(&candidate) {
|
||||
candidate = format!("{node_name}-{counter}");
|
||||
counter += 1;
|
||||
}
|
||||
|
||||
warn!(
|
||||
original = %node_name,
|
||||
adjusted = %candidate,
|
||||
"Duplicate node name detected."
|
||||
);
|
||||
|
||||
names.insert(candidate.clone());
|
||||
candidate
|
||||
}
|
||||
|
||||
pub fn ensure_value_is_not_empty(value: &str) -> Result<(), anyhow::Error> {
|
||||
if value.is_empty() {
|
||||
Err(ValidationError::CantBeEmpty().into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ensure_port_unique(
|
||||
port: Port,
|
||||
validation_context: Rc<RefCell<ValidationContext>>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let mut context = validation_context
|
||||
.try_borrow_mut()
|
||||
.expect(&format!("{BORROWABLE}, {THIS_IS_A_BUG}"));
|
||||
|
||||
if !context.used_ports.contains(&port) {
|
||||
context.used_ports.push(port);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(ValidationError::PortAlreadyUsed(port).into())
|
||||
}
|
||||
|
||||
pub fn generate_unique_para_id(
|
||||
para_id: ParaId,
|
||||
validation_context: Rc<RefCell<ValidationContext>>,
|
||||
) -> String {
|
||||
let mut context = validation_context
|
||||
.try_borrow_mut()
|
||||
.expect(&format!("{BORROWABLE}, {THIS_IS_A_BUG}"));
|
||||
|
||||
if let Some(suffix) = context.used_para_ids.get_mut(¶_id) {
|
||||
*suffix += 1;
|
||||
format!("{para_id}-{suffix}")
|
||||
} else {
|
||||
// insert 0, since will be used next time.
|
||||
context.used_para_ids.insert(para_id, 0);
|
||||
para_id.to_string()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Helper to define states of a type.
|
||||
// We use an enum with no variants because it can't be constructed by definition.
|
||||
macro_rules! states {
|
||||
($($ident:ident),*) => {
|
||||
$(
|
||||
pub enum $ident {}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use states;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,489 @@
|
||||
use std::error::Error;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use serde::{
|
||||
de::{self},
|
||||
ser::SerializeStruct,
|
||||
Deserialize, Serialize,
|
||||
};
|
||||
use support::constants::{SHOULD_COMPILE, THIS_IS_A_BUG};
|
||||
|
||||
use super::{
|
||||
errors::{ConversionError, FieldError},
|
||||
helpers::merge_errors,
|
||||
};
|
||||
|
||||
/// A resource quantity used to define limits (k8s/podman only).
|
||||
/// It can be constructed from a `&str` or u64, if it fails, it returns a [`ConversionError`].
|
||||
/// Possible optional prefixes are: m, K, M, G, T, P, E, Ki, Mi, Gi, Ti, Pi, Ei
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use zombienet_configuration::shared::resources::ResourceQuantity;
|
||||
///
|
||||
/// let quantity1: ResourceQuantity = "100000".try_into().unwrap();
|
||||
/// let quantity2: ResourceQuantity = "1000m".try_into().unwrap();
|
||||
/// let quantity3: ResourceQuantity = "1Gi".try_into().unwrap();
|
||||
/// let quantity4: ResourceQuantity = 10_000.into();
|
||||
///
|
||||
/// assert_eq!(quantity1.as_str(), "100000");
|
||||
/// assert_eq!(quantity2.as_str(), "1000m");
|
||||
/// assert_eq!(quantity3.as_str(), "1Gi");
|
||||
/// assert_eq!(quantity4.as_str(), "10000");
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ResourceQuantity(String);
|
||||
|
||||
impl ResourceQuantity {
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ResourceQuantity {
|
||||
type Error = ConversionError;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(r"^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$")
|
||||
.expect(&format!("{SHOULD_COMPILE}, {THIS_IS_A_BUG}"));
|
||||
}
|
||||
|
||||
if !RE.is_match(value) {
|
||||
return Err(ConversionError::DoesntMatchRegex {
|
||||
value: value.to_string(),
|
||||
regex: r"^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self(value.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for ResourceQuantity {
|
||||
fn from(value: u64) -> Self {
|
||||
Self(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Resources limits used in the context of podman/k8s.
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct Resources {
|
||||
request_memory: Option<ResourceQuantity>,
|
||||
request_cpu: Option<ResourceQuantity>,
|
||||
limit_memory: Option<ResourceQuantity>,
|
||||
limit_cpu: Option<ResourceQuantity>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ResourcesField {
|
||||
memory: Option<ResourceQuantity>,
|
||||
cpu: Option<ResourceQuantity>,
|
||||
}
|
||||
|
||||
impl Serialize for Resources {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut state = serializer.serialize_struct("Resources", 2)?;
|
||||
|
||||
if self.request_memory.is_some() || self.request_memory.is_some() {
|
||||
state.serialize_field(
|
||||
"requests",
|
||||
&ResourcesField {
|
||||
memory: self.request_memory.clone(),
|
||||
cpu: self.request_cpu.clone(),
|
||||
},
|
||||
)?;
|
||||
} else {
|
||||
state.skip_field("requests")?;
|
||||
}
|
||||
|
||||
if self.limit_memory.is_some() || self.limit_memory.is_some() {
|
||||
state.serialize_field(
|
||||
"limits",
|
||||
&ResourcesField {
|
||||
memory: self.limit_memory.clone(),
|
||||
cpu: self.limit_cpu.clone(),
|
||||
},
|
||||
)?;
|
||||
} else {
|
||||
state.skip_field("limits")?;
|
||||
}
|
||||
|
||||
state.end()
|
||||
}
|
||||
}
|
||||
|
||||
struct ResourcesVisitor;
|
||||
|
||||
impl<'de> de::Visitor<'de> for ResourcesVisitor {
|
||||
type Value = Resources;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a resources object")
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: de::MapAccess<'de>,
|
||||
{
|
||||
let mut resources: Resources = Resources::default();
|
||||
|
||||
while let Some((key, value)) = map.next_entry::<String, ResourcesField>()? {
|
||||
match key.as_str() {
|
||||
"requests" => {
|
||||
resources.request_memory = value.memory;
|
||||
resources.request_cpu = value.cpu;
|
||||
},
|
||||
"limits" => {
|
||||
resources.limit_memory = value.memory;
|
||||
resources.limit_cpu = value.cpu;
|
||||
},
|
||||
_ => {
|
||||
return Err(de::Error::unknown_field(
|
||||
&key,
|
||||
&["requests", "limits", "cpu", "memory"],
|
||||
))
|
||||
},
|
||||
}
|
||||
}
|
||||
Ok(resources)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Resources {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_any(ResourcesVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resources {
|
||||
/// Memory limit applied to requests.
|
||||
pub fn request_memory(&self) -> Option<&ResourceQuantity> {
|
||||
self.request_memory.as_ref()
|
||||
}
|
||||
|
||||
/// CPU limit applied to requests.
|
||||
pub fn request_cpu(&self) -> Option<&ResourceQuantity> {
|
||||
self.request_cpu.as_ref()
|
||||
}
|
||||
|
||||
/// Overall memory limit applied.
|
||||
pub fn limit_memory(&self) -> Option<&ResourceQuantity> {
|
||||
self.limit_memory.as_ref()
|
||||
}
|
||||
|
||||
/// Overall CPU limit applied.
|
||||
pub fn limit_cpu(&self) -> Option<&ResourceQuantity> {
|
||||
self.limit_cpu.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// A resources builder, used to build a [`Resources`] declaratively with fields validation.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ResourcesBuilder {
|
||||
config: Resources,
|
||||
errors: Vec<anyhow::Error>,
|
||||
}
|
||||
|
||||
impl ResourcesBuilder {
|
||||
pub fn new() -> ResourcesBuilder {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn transition(config: Resources, errors: Vec<anyhow::Error>) -> Self {
|
||||
Self { config, errors }
|
||||
}
|
||||
|
||||
/// Set the requested memory for a pod. This is the minimum memory allocated for a pod.
|
||||
pub fn with_request_memory<T>(self, quantity: T) -> Self
|
||||
where
|
||||
T: TryInto<ResourceQuantity>,
|
||||
T::Error: Error + Send + Sync + 'static,
|
||||
{
|
||||
match quantity.try_into() {
|
||||
Ok(quantity) => Self::transition(
|
||||
Resources {
|
||||
request_memory: Some(quantity),
|
||||
..self.config
|
||||
},
|
||||
self.errors,
|
||||
),
|
||||
Err(error) => Self::transition(
|
||||
self.config,
|
||||
merge_errors(self.errors, FieldError::RequestMemory(error.into()).into()),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the requested CPU limit for a pod. This is the minimum CPU allocated for a pod.
|
||||
pub fn with_request_cpu<T>(self, quantity: T) -> Self
|
||||
where
|
||||
T: TryInto<ResourceQuantity>,
|
||||
T::Error: Error + Send + Sync + 'static,
|
||||
{
|
||||
match quantity.try_into() {
|
||||
Ok(quantity) => Self::transition(
|
||||
Resources {
|
||||
request_cpu: Some(quantity),
|
||||
..self.config
|
||||
},
|
||||
self.errors,
|
||||
),
|
||||
Err(error) => Self::transition(
|
||||
self.config,
|
||||
merge_errors(self.errors, FieldError::RequestCpu(error.into()).into()),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the overall memory limit for a pod. This is the maximum memory threshold for a pod.
|
||||
pub fn with_limit_memory<T>(self, quantity: T) -> Self
|
||||
where
|
||||
T: TryInto<ResourceQuantity>,
|
||||
T::Error: Error + Send + Sync + 'static,
|
||||
{
|
||||
match quantity.try_into() {
|
||||
Ok(quantity) => Self::transition(
|
||||
Resources {
|
||||
limit_memory: Some(quantity),
|
||||
..self.config
|
||||
},
|
||||
self.errors,
|
||||
),
|
||||
Err(error) => Self::transition(
|
||||
self.config,
|
||||
merge_errors(self.errors, FieldError::LimitMemory(error.into()).into()),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the overall CPU limit for a pod. This is the maximum CPU threshold for a pod.
|
||||
pub fn with_limit_cpu<T>(self, quantity: T) -> Self
|
||||
where
|
||||
T: TryInto<ResourceQuantity>,
|
||||
T::Error: Error + Send + Sync + 'static,
|
||||
{
|
||||
match quantity.try_into() {
|
||||
Ok(quantity) => Self::transition(
|
||||
Resources {
|
||||
limit_cpu: Some(quantity),
|
||||
..self.config
|
||||
},
|
||||
self.errors,
|
||||
),
|
||||
Err(error) => Self::transition(
|
||||
self.config,
|
||||
merge_errors(self.errors, FieldError::LimitCpu(error.into()).into()),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Seals the builder and returns a [`Resources`] if there are no validation errors, else returns errors.
|
||||
pub fn build(self) -> Result<Resources, Vec<anyhow::Error>> {
|
||||
if !self.errors.is_empty() {
|
||||
return Err(self.errors);
|
||||
}
|
||||
|
||||
Ok(self.config)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(non_snake_case)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::NetworkConfig;
|
||||
|
||||
macro_rules! impl_resources_quantity_unit_test {
|
||||
($val:literal) => {{
|
||||
let resources = ResourcesBuilder::new()
|
||||
.with_request_memory($val)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resources.request_memory().unwrap().as_str(), $val);
|
||||
assert_eq!(resources.request_cpu(), None);
|
||||
assert_eq!(resources.limit_cpu(), None);
|
||||
assert_eq!(resources.limit_memory(), None);
|
||||
}};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_string_a_resource_quantity_without_unit_should_succeeds() {
|
||||
impl_resources_quantity_unit_test!("1000");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_with_m_unit_into_a_resource_quantity_should_succeeds() {
|
||||
impl_resources_quantity_unit_test!("100m");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_with_K_unit_into_a_resource_quantity_should_succeeds() {
|
||||
impl_resources_quantity_unit_test!("50K");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_with_M_unit_into_a_resource_quantity_should_succeeds() {
|
||||
impl_resources_quantity_unit_test!("100M");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_with_G_unit_into_a_resource_quantity_should_succeeds() {
|
||||
impl_resources_quantity_unit_test!("1G");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_with_T_unit_into_a_resource_quantity_should_succeeds() {
|
||||
impl_resources_quantity_unit_test!("0.01T");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_with_P_unit_into_a_resource_quantity_should_succeeds() {
|
||||
impl_resources_quantity_unit_test!("0.00001P");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_with_E_unit_into_a_resource_quantity_should_succeeds() {
|
||||
impl_resources_quantity_unit_test!("0.000000001E");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_with_Ki_unit_into_a_resource_quantity_should_succeeds() {
|
||||
impl_resources_quantity_unit_test!("50Ki");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_with_Mi_unit_into_a_resource_quantity_should_succeeds() {
|
||||
impl_resources_quantity_unit_test!("100Mi");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_with_Gi_unit_into_a_resource_quantity_should_succeeds() {
|
||||
impl_resources_quantity_unit_test!("1Gi");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_with_Ti_unit_into_a_resource_quantity_should_succeeds() {
|
||||
impl_resources_quantity_unit_test!("0.01Ti");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_with_Pi_unit_into_a_resource_quantity_should_succeeds() {
|
||||
impl_resources_quantity_unit_test!("0.00001Pi");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_with_Ei_unit_into_a_resource_quantity_should_succeeds() {
|
||||
impl_resources_quantity_unit_test!("0.000000001Ei");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resources_config_builder_should_succeeds_and_returns_a_resources_config() {
|
||||
let resources = ResourcesBuilder::new()
|
||||
.with_request_memory("200M")
|
||||
.with_request_cpu("1G")
|
||||
.with_limit_cpu("500M")
|
||||
.with_limit_memory("2G")
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resources.request_memory().unwrap().as_str(), "200M");
|
||||
assert_eq!(resources.request_cpu().unwrap().as_str(), "1G");
|
||||
assert_eq!(resources.limit_cpu().unwrap().as_str(), "500M");
|
||||
assert_eq!(resources.limit_memory().unwrap().as_str(), "2G");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resources_config_toml_import_should_succeeds_and_returns_a_resources_config() {
|
||||
let load_from_toml =
|
||||
NetworkConfig::load_from_toml("./testing/snapshots/0001-big-network.toml").unwrap();
|
||||
|
||||
let resources = load_from_toml.relaychain().default_resources().unwrap();
|
||||
assert_eq!(resources.request_memory().unwrap().as_str(), "500M");
|
||||
assert_eq!(resources.request_cpu().unwrap().as_str(), "100000");
|
||||
assert_eq!(resources.limit_cpu().unwrap().as_str(), "10Gi");
|
||||
assert_eq!(resources.limit_memory().unwrap().as_str(), "4000M");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resources_config_builder_should_fails_and_returns_an_error_if_couldnt_parse_request_memory()
|
||||
{
|
||||
let resources_builder = ResourcesBuilder::new().with_request_memory("invalid");
|
||||
|
||||
let errors = resources_builder.build().err().unwrap();
|
||||
|
||||
assert_eq!(errors.len(), 1);
|
||||
assert_eq!(
|
||||
errors.first().unwrap().to_string(),
|
||||
r"request_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resources_config_builder_should_fails_and_returns_an_error_if_couldnt_parse_request_cpu() {
|
||||
let resources_builder = ResourcesBuilder::new().with_request_cpu("invalid");
|
||||
|
||||
let errors = resources_builder.build().err().unwrap();
|
||||
|
||||
assert_eq!(errors.len(), 1);
|
||||
assert_eq!(
|
||||
errors.first().unwrap().to_string(),
|
||||
r"request_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resources_config_builder_should_fails_and_returns_an_error_if_couldnt_parse_limit_memory() {
|
||||
let resources_builder = ResourcesBuilder::new().with_limit_memory("invalid");
|
||||
|
||||
let errors = resources_builder.build().err().unwrap();
|
||||
|
||||
assert_eq!(errors.len(), 1);
|
||||
assert_eq!(
|
||||
errors.first().unwrap().to_string(),
|
||||
r"limit_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resources_config_builder_should_fails_and_returns_an_error_if_couldnt_parse_limit_cpu() {
|
||||
let resources_builder = ResourcesBuilder::new().with_limit_cpu("invalid");
|
||||
|
||||
let errors = resources_builder.build().err().unwrap();
|
||||
|
||||
assert_eq!(errors.len(), 1);
|
||||
assert_eq!(
|
||||
errors.first().unwrap().to_string(),
|
||||
r"limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resources_config_builder_should_fails_and_returns_multiple_error_if_couldnt_parse_multiple_fields(
|
||||
) {
|
||||
let resources_builder = ResourcesBuilder::new()
|
||||
.with_limit_cpu("invalid")
|
||||
.with_request_memory("invalid");
|
||||
|
||||
let errors = resources_builder.build().err().unwrap();
|
||||
|
||||
assert_eq!(errors.len(), 2);
|
||||
assert_eq!(
|
||||
errors.first().unwrap().to_string(),
|
||||
r"limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
|
||||
);
|
||||
assert_eq!(
|
||||
errors.get(1).unwrap().to_string(),
|
||||
r"request_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,930 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
error::Error,
|
||||
fmt::{self, Display},
|
||||
path::PathBuf,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use serde::{
|
||||
de::{self, IntoDeserializer},
|
||||
Deserialize, Deserializer, Serialize,
|
||||
};
|
||||
use support::constants::{INFAILABLE, SHOULD_COMPILE, THIS_IS_A_BUG};
|
||||
use tokio::fs;
|
||||
use url::Url;
|
||||
|
||||
use super::{errors::ConversionError, resources::Resources};
|
||||
|
||||
/// An alias for a duration in seconds.
|
||||
pub type Duration = u32;
|
||||
|
||||
/// An alias for a port.
|
||||
pub type Port = u16;
|
||||
|
||||
/// An alias for a parachain ID.
|
||||
pub type ParaId = u32;
|
||||
|
||||
/// Custom type wrapping u128 to add custom Serialization/Deserialization logic because it's not supported
|
||||
/// issue tracking the problem: <https://github.com/toml-rs/toml/issues/540>
|
||||
#[derive(Default, Debug, Clone, PartialEq)]
|
||||
pub struct U128(pub(crate) u128);
|
||||
|
||||
impl From<u128> for U128 {
|
||||
fn from(value: u128) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for U128 {
|
||||
type Error = Box<dyn Error>;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
Ok(Self(value.to_string().parse::<u128>()?))
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for U128 {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
// here we add a prefix to the string to be able to replace the wrapped
|
||||
// value with "" to a value without "" in the TOML string
|
||||
serializer.serialize_str(&format!("U128%{}", self.0))
|
||||
}
|
||||
}
|
||||
|
||||
struct U128Visitor;
|
||||
|
||||
impl de::Visitor<'_> for U128Visitor {
|
||||
type Value = U128;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("an integer between 0 and 2^128 − 1.")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
v.try_into().map_err(de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for U128 {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_str(U128Visitor)
|
||||
}
|
||||
}
|
||||
|
||||
/// A chain name.
|
||||
/// It can be constructed for an `&str`, if it fails, it will returns a [`ConversionError`].
|
||||
///
|
||||
/// # Examples:
|
||||
/// ```
|
||||
/// use zombienet_configuration::shared::types::Chain;
|
||||
///
|
||||
/// let polkadot: Chain = "polkadot".try_into().unwrap();
|
||||
/// let kusama: Chain = "kusama".try_into().unwrap();
|
||||
/// let myparachain: Chain = "myparachain".try_into().unwrap();
|
||||
///
|
||||
/// assert_eq!(polkadot.as_str(), "polkadot");
|
||||
/// assert_eq!(kusama.as_str(), "kusama");
|
||||
/// assert_eq!(myparachain.as_str(), "myparachain");
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Chain(String);
|
||||
|
||||
impl TryFrom<&str> for Chain {
|
||||
type Error = ConversionError;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
if value.contains(char::is_whitespace) {
|
||||
return Err(ConversionError::ContainsWhitespaces(value.to_string()));
|
||||
}
|
||||
|
||||
if value.is_empty() {
|
||||
return Err(ConversionError::CantBeEmpty);
|
||||
}
|
||||
|
||||
Ok(Self(value.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Chain {
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A container image.
|
||||
/// It can be constructed from an `&str` including a combination of name, version, IPv4 or/and hostname, if it fails, it will returns a [`ConversionError`].
|
||||
///
|
||||
/// # Examples:
|
||||
/// ```
|
||||
/// use zombienet_configuration::shared::types::Image;
|
||||
///
|
||||
/// let image1: Image = "name".try_into().unwrap();
|
||||
/// let image2: Image = "name:version".try_into().unwrap();
|
||||
/// let image3: Image = "myrepo.com/name:version".try_into().unwrap();
|
||||
/// let image4: Image = "10.15.43.155/name:version".try_into().unwrap();
|
||||
///
|
||||
/// assert_eq!(image1.as_str(), "name");
|
||||
/// assert_eq!(image2.as_str(), "name:version");
|
||||
/// assert_eq!(image3.as_str(), "myrepo.com/name:version");
|
||||
/// assert_eq!(image4.as_str(), "10.15.43.155/name:version");
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Image(String);
|
||||
|
||||
impl TryFrom<&str> for Image {
|
||||
type Error = ConversionError;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
static IP_PART: &str = "((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]).){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))";
|
||||
static HOSTNAME_PART: &str = "((([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]).)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9]))";
|
||||
static TAG_NAME_PART: &str = "([a-z0-9](-*[a-z0-9])*)";
|
||||
static TAG_VERSION_PART: &str = "([a-z0-9_]([-._a-z0-9])*)";
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(&format!(
|
||||
"^({IP_PART}|{HOSTNAME_PART}/)?{TAG_NAME_PART}(:{TAG_VERSION_PART})?$",
|
||||
))
|
||||
.expect(&format!("{SHOULD_COMPILE}, {THIS_IS_A_BUG}"));
|
||||
};
|
||||
|
||||
if !RE.is_match(value) {
|
||||
return Err(ConversionError::DoesntMatchRegex {
|
||||
value: value.to_string(),
|
||||
regex: "^([ip]|[hostname]/)?[tag_name]:[tag_version]?$".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self(value.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Image {
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A command that will be executed natively (native provider) or in a container (podman/k8s).
|
||||
/// It can be constructed from an `&str`, if it fails, it will returns a [`ConversionError`].
|
||||
///
|
||||
/// # Examples:
|
||||
/// ```
|
||||
/// use zombienet_configuration::shared::types::Command;
|
||||
///
|
||||
/// let command1: Command = "mycommand".try_into().unwrap();
|
||||
/// let command2: Command = "myothercommand".try_into().unwrap();
|
||||
///
|
||||
/// assert_eq!(command1.as_str(), "mycommand");
|
||||
/// assert_eq!(command2.as_str(), "myothercommand");
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Command(String);
|
||||
|
||||
impl TryFrom<&str> for Command {
|
||||
type Error = ConversionError;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
if value.contains(char::is_whitespace) {
|
||||
return Err(ConversionError::ContainsWhitespaces(value.to_string()));
|
||||
}
|
||||
|
||||
Ok(Self(value.to_string()))
|
||||
}
|
||||
}
|
||||
impl Default for Command {
|
||||
fn default() -> Self {
|
||||
Self(String::from("polkadot"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A command with optional custom arguments, the command will be executed natively (native provider) or in a container (podman/k8s).
|
||||
/// It can be constructed from an `&str`, if it fails, it will returns a [`ConversionError`].
|
||||
///
|
||||
/// # Examples:
|
||||
/// ```
|
||||
/// use zombienet_configuration::shared::types::CommandWithCustomArgs;
|
||||
///
|
||||
/// let command1: CommandWithCustomArgs = "mycommand --demo=2 --other-flag".try_into().unwrap();
|
||||
/// let command2: CommandWithCustomArgs = "my_other_cmd_without_args".try_into().unwrap();
|
||||
///
|
||||
/// assert_eq!(command1.cmd().as_str(), "mycommand");
|
||||
/// assert_eq!(command2.cmd().as_str(), "my_other_cmd_without_args");
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct CommandWithCustomArgs(Command, Vec<Arg>);
|
||||
|
||||
impl TryFrom<&str> for CommandWithCustomArgs {
|
||||
type Error = ConversionError;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
if value.is_empty() {
|
||||
return Err(ConversionError::CantBeEmpty);
|
||||
}
|
||||
|
||||
let mut parts = value.split_whitespace().collect::<Vec<&str>>();
|
||||
let cmd = parts.remove(0).try_into().unwrap();
|
||||
let args = parts
|
||||
.iter()
|
||||
.map(|x| {
|
||||
Arg::deserialize(x.into_deserializer()).map_err(|_: serde_json::Error| {
|
||||
ConversionError::DeserializeError(String::from(*x))
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<Arg>, _>>()?;
|
||||
|
||||
Ok(Self(cmd, args))
|
||||
}
|
||||
}
|
||||
impl Default for CommandWithCustomArgs {
|
||||
fn default() -> Self {
|
||||
Self("polkadot".try_into().unwrap(), vec![])
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandWithCustomArgs {
|
||||
pub fn cmd(&self) -> &Command {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn args(&self) -> &Vec<Arg> {
|
||||
&self.1
|
||||
}
|
||||
}
|
||||
|
||||
/// A location for a locally or remotely stored asset.
|
||||
/// It can be constructed from an [`url::Url`], a [`std::path::PathBuf`] or an `&str`.
|
||||
///
|
||||
/// # Examples:
|
||||
/// ```
|
||||
/// use url::Url;
|
||||
/// use std::{path::PathBuf, str::FromStr};
|
||||
/// use zombienet_configuration::shared::types::AssetLocation;
|
||||
///
|
||||
/// let url_location: AssetLocation = Url::from_str("https://mycloudstorage.com/path/to/my/file.tgz").unwrap().into();
|
||||
/// let url_location2: AssetLocation = "https://mycloudstorage.com/path/to/my/file.tgz".into();
|
||||
/// let path_location: AssetLocation = PathBuf::from_str("/tmp/path/to/my/file").unwrap().into();
|
||||
/// let path_location2: AssetLocation = "/tmp/path/to/my/file".into();
|
||||
///
|
||||
/// assert!(matches!(url_location, AssetLocation::Url(value) if value.as_str() == "https://mycloudstorage.com/path/to/my/file.tgz"));
|
||||
/// assert!(matches!(url_location2, AssetLocation::Url(value) if value.as_str() == "https://mycloudstorage.com/path/to/my/file.tgz"));
|
||||
/// assert!(matches!(path_location, AssetLocation::FilePath(value) if value.to_str().unwrap() == "/tmp/path/to/my/file"));
|
||||
/// assert!(matches!(path_location2, AssetLocation::FilePath(value) if value.to_str().unwrap() == "/tmp/path/to/my/file"));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum AssetLocation {
|
||||
Url(Url),
|
||||
FilePath(PathBuf),
|
||||
}
|
||||
|
||||
impl From<Url> for AssetLocation {
|
||||
fn from(value: Url) -> Self {
|
||||
Self::Url(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PathBuf> for AssetLocation {
|
||||
fn from(value: PathBuf) -> Self {
|
||||
Self::FilePath(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for AssetLocation {
|
||||
fn from(value: &str) -> Self {
|
||||
if let Ok(parsed_url) = Url::parse(value) {
|
||||
return Self::Url(parsed_url);
|
||||
}
|
||||
|
||||
Self::FilePath(PathBuf::from_str(value).expect(&format!("{INFAILABLE}, {THIS_IS_A_BUG}")))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for AssetLocation {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
AssetLocation::Url(value) => write!(f, "{}", value.as_str()),
|
||||
AssetLocation::FilePath(value) => write!(f, "{}", value.display()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AssetLocation {
|
||||
/// Get the current asset (from file or url) and return the content
|
||||
pub async fn get_asset(&self) -> Result<Vec<u8>, anyhow::Error> {
|
||||
let contents = match self {
|
||||
AssetLocation::Url(location) => {
|
||||
let res = reqwest::get(location.as_ref()).await.map_err(|err| {
|
||||
anyhow!("Error dowinloding asset from url {location} - {err}")
|
||||
})?;
|
||||
|
||||
res.bytes().await.unwrap().into()
|
||||
},
|
||||
AssetLocation::FilePath(filepath) => {
|
||||
tokio::fs::read(filepath).await.map_err(|err| {
|
||||
anyhow!(
|
||||
"Error reading asset from path {} - {}",
|
||||
filepath.to_string_lossy(),
|
||||
err
|
||||
)
|
||||
})?
|
||||
},
|
||||
};
|
||||
|
||||
Ok(contents)
|
||||
}
|
||||
|
||||
/// Write asset (from file or url) to the destination path.
|
||||
pub async fn dump_asset(&self, dst_path: impl Into<PathBuf>) -> Result<(), anyhow::Error> {
|
||||
let contents = self.get_asset().await?;
|
||||
fs::write(dst_path.into(), contents).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for AssetLocation {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
struct AssetLocationVisitor;
|
||||
|
||||
impl de::Visitor<'_> for AssetLocationVisitor {
|
||||
type Value = AssetLocation;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a string")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(AssetLocation::from(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for AssetLocation {
|
||||
fn deserialize<D>(deserializer: D) -> Result<AssetLocation, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_any(AssetLocationVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
/// A CLI argument passed to an executed command, can be an option with an assigned value or a simple flag to enable/disable a feature.
|
||||
/// A flag arg can be constructed from a `&str` and a option arg can be constructed from a `(&str, &str)`.
|
||||
///
|
||||
/// # Examples:
|
||||
/// ```
|
||||
/// use zombienet_configuration::shared::types::Arg;
|
||||
///
|
||||
/// let flag_arg: Arg = "myflag".into();
|
||||
/// let option_arg: Arg = ("name", "value").into();
|
||||
///
|
||||
/// assert!(matches!(flag_arg, Arg::Flag(value) if value == "myflag"));
|
||||
/// assert!(matches!(option_arg, Arg::Option(name, value) if name == "name" && value == "value"));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Arg {
|
||||
Flag(String),
|
||||
Option(String, String),
|
||||
Array(String, Vec<String>),
|
||||
}
|
||||
|
||||
impl From<&str> for Arg {
|
||||
fn from(flag: &str) -> Self {
|
||||
Self::Flag(flag.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&str, &str)> for Arg {
|
||||
fn from((option, value): (&str, &str)) -> Self {
|
||||
Self::Option(option.to_owned(), value.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<(&str, &[T])> for Arg
|
||||
where
|
||||
T: AsRef<str> + Clone,
|
||||
{
|
||||
fn from((option, values): (&str, &[T])) -> Self {
|
||||
Self::Array(
|
||||
option.to_owned(),
|
||||
values.iter().map(|v| v.as_ref().to_string()).collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<(&str, Vec<T>)> for Arg
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
fn from((option, values): (&str, Vec<T>)) -> Self {
|
||||
Self::Array(
|
||||
option.to_owned(),
|
||||
values.into_iter().map(|v| v.as_ref().to_string()).collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Arg {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
Arg::Flag(value) => serializer.serialize_str(value),
|
||||
Arg::Option(option, value) => serializer.serialize_str(&format!("{option}={value}")),
|
||||
Arg::Array(option, values) => {
|
||||
serializer.serialize_str(&format!("{}=[{}]", option, values.join(",")))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ArgVisitor;
|
||||
|
||||
impl de::Visitor<'_> for ArgVisitor {
|
||||
type Value = Arg;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a string")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
// covers the "-lruntime=debug,parachain=trace" case
|
||||
// TODO: Make this more generic by adding the scenario in the regex below
|
||||
if v.starts_with("-l") || v.starts_with("-log") {
|
||||
return Ok(Arg::Flag(v.to_string()));
|
||||
}
|
||||
// Handle argument removal syntax: -:--flag-name
|
||||
if v.starts_with("-:") {
|
||||
return Ok(Arg::Flag(v.to_string()));
|
||||
}
|
||||
let re = Regex::new("^(?<name_prefix>(?<prefix>-{1,2})?(?<name>[a-zA-Z]+(-[a-zA-Z]+)*))((?<separator>=| )(?<value>\\[[^\\]]*\\]|[^ ]+))?$").unwrap();
|
||||
|
||||
let captures = re.captures(v);
|
||||
if let Some(captures) = captures {
|
||||
if let Some(value) = captures.name("value") {
|
||||
let name_prefix = captures
|
||||
.name("name_prefix")
|
||||
.expect("BUG: name_prefix capture group missing")
|
||||
.as_str()
|
||||
.to_string();
|
||||
|
||||
let val = value.as_str();
|
||||
if val.starts_with('[') && val.ends_with(']') {
|
||||
// Remove brackets and split by comma
|
||||
let inner = &val[1..val.len() - 1];
|
||||
let items: Vec<String> = inner
|
||||
.split(',')
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
return Ok(Arg::Array(name_prefix, items));
|
||||
} else {
|
||||
return Ok(Arg::Option(name_prefix, val.to_string()));
|
||||
}
|
||||
}
|
||||
if let Some(name_prefix) = captures.name("name_prefix") {
|
||||
return Ok(Arg::Flag(name_prefix.as_str().to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
Err(de::Error::custom(
|
||||
"the provided argument is invalid and doesn't match Arg::Option, Arg::Flag or Arg::Array",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Arg {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_any(ArgVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct ValidationContext {
|
||||
pub used_ports: Vec<Port>,
|
||||
pub used_nodes_names: HashSet<String>,
|
||||
// Store para_id already used
|
||||
pub used_para_ids: HashMap<ParaId, u8>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Deserialize)]
|
||||
pub struct ChainDefaultContext {
|
||||
pub(crate) default_command: Option<Command>,
|
||||
pub(crate) default_image: Option<Image>,
|
||||
pub(crate) default_resources: Option<Resources>,
|
||||
pub(crate) default_db_snapshot: Option<AssetLocation>,
|
||||
#[serde(default)]
|
||||
pub(crate) default_args: Vec<Arg>,
|
||||
}
|
||||
|
||||
/// Represent a runtime (.wasm) asset location and an
|
||||
/// optional preset to use for chain-spec generation.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ChainSpecRuntime {
|
||||
pub location: AssetLocation,
|
||||
pub preset: Option<String>,
|
||||
}
|
||||
|
||||
impl ChainSpecRuntime {
|
||||
pub fn new(location: AssetLocation) -> Self {
|
||||
ChainSpecRuntime {
|
||||
location,
|
||||
preset: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_preset(location: AssetLocation, preset: impl Into<String>) -> Self {
|
||||
ChainSpecRuntime {
|
||||
location,
|
||||
preset: Some(preset.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a set of JSON overrides for a configuration.
|
||||
///
|
||||
/// The overrides can be provided as an inline JSON object or loaded from a
|
||||
/// separate file via a path or URL.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum JsonOverrides {
|
||||
/// A path or URL pointing to a JSON file containing the overrides.
|
||||
Location(AssetLocation),
|
||||
/// An inline JSON value representing the overrides.
|
||||
Json(serde_json::Value),
|
||||
}
|
||||
|
||||
impl From<AssetLocation> for JsonOverrides {
|
||||
fn from(value: AssetLocation) -> Self {
|
||||
Self::Location(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Value> for JsonOverrides {
|
||||
fn from(value: serde_json::Value) -> Self {
|
||||
Self::Json(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for JsonOverrides {
|
||||
fn from(value: &str) -> Self {
|
||||
Self::Location(AssetLocation::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for JsonOverrides {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
JsonOverrides::Location(location) => write!(f, "{location}"),
|
||||
JsonOverrides::Json(json) => write!(f, "{json}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonOverrides {
|
||||
pub async fn get(&self) -> Result<serde_json::Value, anyhow::Error> {
|
||||
let contents = match self {
|
||||
Self::Location(location) => serde_json::from_slice(&location.get_asset().await?)
|
||||
.map_err(|err| anyhow!("Error converting asset to json {location} - {err}")),
|
||||
Self::Json(json) => Ok(json.clone()),
|
||||
};
|
||||
|
||||
contents
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_arg_flag_roundtrip() {
|
||||
let arg = Arg::from("verbose");
|
||||
let serialized = serde_json::to_string(&arg).unwrap();
|
||||
let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
|
||||
assert_eq!(arg, deserialized);
|
||||
}
|
||||
#[test]
|
||||
fn test_arg_option_roundtrip() {
|
||||
let arg = Arg::from(("mode", "fast"));
|
||||
let serialized = serde_json::to_string(&arg).unwrap();
|
||||
let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
|
||||
assert_eq!(arg, deserialized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arg_array_roundtrip() {
|
||||
let arg = Arg::from(("items", ["a", "b", "c"].as_slice()));
|
||||
|
||||
let serialized = serde_json::to_string(&arg).unwrap();
|
||||
println!("serialized = {serialized}");
|
||||
let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
|
||||
assert_eq!(arg, deserialized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arg_option_valid_input() {
|
||||
let expected = Arg::from(("--foo", "bar"));
|
||||
|
||||
// name and value delimited with =
|
||||
let valid = "\"--foo=bar\"";
|
||||
let result: Result<Arg, _> = serde_json::from_str(valid);
|
||||
assert_eq!(result.unwrap(), expected);
|
||||
|
||||
// name and value delimited with space
|
||||
let valid = "\"--foo bar\"";
|
||||
let result: Result<Arg, _> = serde_json::from_str(valid);
|
||||
assert_eq!(result.unwrap(), expected);
|
||||
|
||||
// value contains =
|
||||
let expected = Arg::from(("--foo", "bar=baz"));
|
||||
let valid = "\"--foo=bar=baz\"";
|
||||
let result: Result<Arg, _> = serde_json::from_str(valid);
|
||||
assert_eq!(result.unwrap(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arg_array_valid_input() {
|
||||
let expected = Arg::from(("--foo", vec!["bar", "baz"]));
|
||||
|
||||
// name and values delimited with =
|
||||
let valid = "\"--foo=[bar,baz]\"";
|
||||
let result: Result<Arg, _> = serde_json::from_str(valid);
|
||||
assert_eq!(result.unwrap(), expected);
|
||||
|
||||
// name and values delimited with space
|
||||
let valid = "\"--foo [bar,baz]\"";
|
||||
let result: Result<Arg, _> = serde_json::from_str(valid);
|
||||
assert_eq!(result.unwrap(), expected);
|
||||
|
||||
// values delimited with commas and space
|
||||
let valid = "\"--foo [bar , baz]\"";
|
||||
let result: Result<Arg, _> = serde_json::from_str(valid);
|
||||
assert_eq!(result.unwrap(), expected);
|
||||
|
||||
// empty values array
|
||||
let expected = Arg::from(("--foo", Vec::<&str>::new()));
|
||||
let valid = "\"--foo []\"";
|
||||
let result: Result<Arg, _> = serde_json::from_str(valid);
|
||||
assert_eq!(result.unwrap(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arg_invalid_input() {
|
||||
// missing = or space
|
||||
let invalid = "\"--foo[bar]\"";
|
||||
let result: Result<Arg, _> = serde_json::from_str(invalid);
|
||||
assert!(result.is_err());
|
||||
|
||||
// value contains space
|
||||
let invalid = "\"--foo=bar baz\"";
|
||||
let result: Result<Arg, _> = serde_json::from_str(invalid);
|
||||
println!("result = {result:?}");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_without_whitespaces_into_a_chain_should_succeeds() {
|
||||
let got: Result<Chain, ConversionError> = "mychain".try_into();
|
||||
|
||||
assert_eq!(got.unwrap().as_str(), "mychain");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_containing_tag_name_into_an_image_should_succeeds() {
|
||||
let got: Result<Image, ConversionError> = "myimage".try_into();
|
||||
|
||||
assert_eq!(got.unwrap().as_str(), "myimage");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_containing_tag_name_and_tag_version_into_an_image_should_succeeds() {
|
||||
let got: Result<Image, ConversionError> = "myimage:version".try_into();
|
||||
|
||||
assert_eq!(got.unwrap().as_str(), "myimage:version");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_containing_hostname_and_tag_name_into_an_image_should_succeeds() {
|
||||
let got: Result<Image, ConversionError> = "myrepository.com/myimage".try_into();
|
||||
|
||||
assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_containing_hostname_tag_name_and_tag_version_into_an_image_should_succeeds()
|
||||
{
|
||||
let got: Result<Image, ConversionError> = "myrepository.com/myimage:version".try_into();
|
||||
|
||||
assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage:version");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_containing_ip_and_tag_name_into_an_image_should_succeeds() {
|
||||
let got: Result<Image, ConversionError> = "myrepository.com/myimage".try_into();
|
||||
|
||||
assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_containing_ip_tag_name_and_tag_version_into_an_image_should_succeeds() {
|
||||
let got: Result<Image, ConversionError> = "127.0.0.1/myimage:version".try_into();
|
||||
|
||||
assert_eq!(got.unwrap().as_str(), "127.0.0.1/myimage:version");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_without_whitespaces_into_a_command_should_succeeds() {
|
||||
let got: Result<Command, ConversionError> = "mycommand".try_into();
|
||||
|
||||
assert_eq!(got.unwrap().as_str(), "mycommand");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_an_url_into_an_asset_location_should_succeeds() {
|
||||
let url = Url::from_str("https://mycloudstorage.com/path/to/my/file.tgz").unwrap();
|
||||
let got: AssetLocation = url.clone().into();
|
||||
|
||||
assert!(matches!(got, AssetLocation::Url(value) if value == url));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_pathbuf_into_an_asset_location_should_succeeds() {
|
||||
let pathbuf = PathBuf::from_str("/tmp/path/to/my/file").unwrap();
|
||||
let got: AssetLocation = pathbuf.clone().into();
|
||||
|
||||
assert!(matches!(got, AssetLocation::FilePath(value) if value == pathbuf));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_into_an_url_asset_location_should_succeeds() {
|
||||
let url = "https://mycloudstorage.com/path/to/my/file.tgz";
|
||||
let got: AssetLocation = url.into();
|
||||
|
||||
assert!(matches!(got, AssetLocation::Url(value) if value == Url::from_str(url).unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_into_an_filepath_asset_location_should_succeeds() {
|
||||
let filepath = "/tmp/path/to/my/file";
|
||||
let got: AssetLocation = filepath.into();
|
||||
|
||||
assert!(matches!(
|
||||
got,
|
||||
AssetLocation::FilePath(value) if value == PathBuf::from_str(filepath).unwrap()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_into_an_flag_arg_should_succeeds() {
|
||||
let got: Arg = "myflag".into();
|
||||
|
||||
assert!(matches!(got, Arg::Flag(flag) if flag == "myflag"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_tuple_into_an_option_arg_should_succeeds() {
|
||||
let got: Arg = ("name", "value").into();
|
||||
|
||||
assert!(matches!(got, Arg::Option(name, value) if name == "name" && value == "value"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_with_whitespaces_into_a_chain_should_fails() {
|
||||
let got: Result<Chain, ConversionError> = "my chain".try_into();
|
||||
|
||||
assert!(matches!(
|
||||
got.clone().unwrap_err(),
|
||||
ConversionError::ContainsWhitespaces(_)
|
||||
));
|
||||
assert_eq!(
|
||||
got.unwrap_err().to_string(),
|
||||
"'my chain' shouldn't contains whitespace"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_an_empty_str_into_a_chain_should_fails() {
|
||||
let got: Result<Chain, ConversionError> = "".try_into();
|
||||
|
||||
assert!(matches!(
|
||||
got.clone().unwrap_err(),
|
||||
ConversionError::CantBeEmpty
|
||||
));
|
||||
assert_eq!(got.unwrap_err().to_string(), "can't be empty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_containing_only_ip_into_an_image_should_fails() {
|
||||
let got: Result<Image, ConversionError> = "127.0.0.1".try_into();
|
||||
|
||||
assert!(matches!(
|
||||
got.clone().unwrap_err(),
|
||||
ConversionError::DoesntMatchRegex { value: _, regex: _ }
|
||||
));
|
||||
assert_eq!(
|
||||
got.unwrap_err().to_string(),
|
||||
"'127.0.0.1' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_containing_only_ip_and_tag_version_into_an_image_should_fails() {
|
||||
let got: Result<Image, ConversionError> = "127.0.0.1:version".try_into();
|
||||
|
||||
assert!(matches!(
|
||||
got.clone().unwrap_err(),
|
||||
ConversionError::DoesntMatchRegex { value: _, regex: _ }
|
||||
));
|
||||
assert_eq!(got.unwrap_err().to_string(), "'127.0.0.1:version' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_containing_only_hostname_into_an_image_should_fails() {
|
||||
let got: Result<Image, ConversionError> = "myrepository.com".try_into();
|
||||
|
||||
assert!(matches!(
|
||||
got.clone().unwrap_err(),
|
||||
ConversionError::DoesntMatchRegex { value: _, regex: _ }
|
||||
));
|
||||
assert_eq!(got.unwrap_err().to_string(), "'myrepository.com' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_containing_only_hostname_and_tag_version_into_an_image_should_fails() {
|
||||
let got: Result<Image, ConversionError> = "myrepository.com:version".try_into();
|
||||
|
||||
assert!(matches!(
|
||||
got.clone().unwrap_err(),
|
||||
ConversionError::DoesntMatchRegex { value: _, regex: _ }
|
||||
));
|
||||
assert_eq!(got.unwrap_err().to_string(), "'myrepository.com:version' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converting_a_str_with_whitespaces_into_a_command_should_fails() {
|
||||
let got: Result<Command, ConversionError> = "my command".try_into();
|
||||
|
||||
assert!(matches!(
|
||||
got.clone().unwrap_err(),
|
||||
ConversionError::ContainsWhitespaces(_)
|
||||
));
|
||||
assert_eq!(
|
||||
got.unwrap_err().to_string(),
|
||||
"'my command' shouldn't contains whitespace"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convert_to_json_overrides() {
|
||||
let url: AssetLocation = "https://example.com/overrides.json".into();
|
||||
assert!(matches!(
|
||||
url.into(),
|
||||
JsonOverrides::Location(AssetLocation::Url(_))
|
||||
));
|
||||
|
||||
let path: AssetLocation = "/path/to/overrides.json".into();
|
||||
assert!(matches!(
|
||||
path.into(),
|
||||
JsonOverrides::Location(AssetLocation::FilePath(_))
|
||||
));
|
||||
|
||||
let inline = serde_json::json!({ "para_id": 2000});
|
||||
assert!(matches!(
|
||||
inline.into(),
|
||||
JsonOverrides::Json(serde_json::Value::Object(_))
|
||||
));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,65 @@
|
||||
use std::env;
|
||||
|
||||
use support::constants::ZOMBIE_NODE_SPAWN_TIMEOUT_SECONDS;
|
||||
|
||||
use crate::types::{Chain, Command, Duration};
|
||||
|
||||
pub(crate) fn is_true(value: &bool) -> bool {
|
||||
*value
|
||||
}
|
||||
|
||||
pub(crate) fn is_false(value: &bool) -> bool {
|
||||
!(*value)
|
||||
}
|
||||
|
||||
pub(crate) fn default_as_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn default_as_false() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub(crate) fn default_initial_balance() -> crate::types::U128 {
|
||||
2_000_000_000_000.into()
|
||||
}
|
||||
|
||||
/// Default timeout for spawning a node (10mins)
|
||||
pub(crate) fn default_node_spawn_timeout() -> Duration {
|
||||
env::var(ZOMBIE_NODE_SPAWN_TIMEOUT_SECONDS)
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<u32>().ok())
|
||||
.unwrap_or(600)
|
||||
}
|
||||
|
||||
/// Default timeout for spawning the whole network (1hr)
|
||||
pub(crate) fn default_timeout() -> Duration {
|
||||
3600
|
||||
}
|
||||
|
||||
pub(crate) fn default_command_polkadot() -> Option<Command> {
|
||||
TryInto::<Command>::try_into("polkadot").ok()
|
||||
}
|
||||
|
||||
pub(crate) fn default_relaychain_chain() -> Chain {
|
||||
TryInto::<Chain>::try_into("rococo-local").expect("'rococo-local' should be a valid chain")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_node_spawn_timeout_works_before_and_after_env_is_set() {
|
||||
// The default should be 600 seconds if the env var is not set
|
||||
assert_eq!(default_node_spawn_timeout(), 600);
|
||||
|
||||
// If env var is set to a valid number, it should return that number
|
||||
env::set_var(ZOMBIE_NODE_SPAWN_TIMEOUT_SECONDS, "123");
|
||||
assert_eq!(default_node_spawn_timeout(), 123);
|
||||
|
||||
// If env var is set to a NOT valid number, it should return 600
|
||||
env::set_var(ZOMBIE_NODE_SPAWN_TIMEOUT_SECONDS, "NOT_A_NUMBER");
|
||||
assert_eq!(default_node_spawn_timeout(), 600);
|
||||
}
|
||||
}
|
||||
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
[settings]
|
||||
timeout = 3600
|
||||
node_spawn_timeout = 600
|
||||
tear_down_on_failure = true
|
||||
|
||||
[relaychain]
|
||||
chain = "rococo-local"
|
||||
default_command = "polkadot"
|
||||
default_image = "docker.io/parity/polkadot:latest"
|
||||
default_args = ["-lparachain=debug"]
|
||||
|
||||
[[relaychain.nodes]]
|
||||
name = "alice"
|
||||
validator = true
|
||||
invulnerable = true
|
||||
bootnode = false
|
||||
balance = 2000000000000
|
||||
|
||||
[[relaychain.nodes]]
|
||||
name = "bob"
|
||||
args = ["--database=paritydb-experimental"]
|
||||
validator = true
|
||||
invulnerable = false
|
||||
bootnode = true
|
||||
balance = 2000000000000
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
[settings]
|
||||
timeout = 3600
|
||||
node_spawn_timeout = 600
|
||||
tear_down_on_failure = true
|
||||
|
||||
[relaychain]
|
||||
chain = "polkadot"
|
||||
default_command = "polkadot"
|
||||
default_image = "docker.io/parity/polkadot:latest"
|
||||
|
||||
[relaychain.default_resources.requests]
|
||||
memory = "500M"
|
||||
cpu = "100000"
|
||||
|
||||
[relaychain.default_resources.limits]
|
||||
memory = "4000M"
|
||||
cpu = "10Gi"
|
||||
|
||||
[[relaychain.nodes]]
|
||||
name = "alice"
|
||||
validator = true
|
||||
invulnerable = true
|
||||
bootnode = true
|
||||
balance = 1000000000
|
||||
|
||||
[[relaychain.nodes]]
|
||||
name = "bob"
|
||||
validator = true
|
||||
invulnerable = true
|
||||
bootnode = true
|
||||
balance = 2000000000000
|
||||
|
||||
[[teyrchains]]
|
||||
id = 1000
|
||||
chain = "myparachain"
|
||||
register_para = true
|
||||
onboard_as_teyrchain = false
|
||||
balance = 2000000000000
|
||||
default_db_snapshot = "https://storage.com/path/to/db_snapshot.tgz"
|
||||
chain_spec_path = "/path/to/my/chain/spec.json"
|
||||
cumulus_based = true
|
||||
evm_based = false
|
||||
|
||||
[[teyrchains.collators]]
|
||||
name = "john"
|
||||
validator = true
|
||||
invulnerable = true
|
||||
bootnode = true
|
||||
balance = 5000000000
|
||||
|
||||
[[teyrchains.collators]]
|
||||
name = "charles"
|
||||
validator = false
|
||||
invulnerable = true
|
||||
bootnode = true
|
||||
balance = 0
|
||||
|
||||
[[teyrchains.collators]]
|
||||
name = "frank"
|
||||
validator = true
|
||||
invulnerable = false
|
||||
bootnode = true
|
||||
balance = 1000000000
|
||||
|
||||
[[teyrchains]]
|
||||
id = 2000
|
||||
chain = "myotherparachain"
|
||||
add_to_genesis = true
|
||||
balance = 2000000000000
|
||||
chain_spec_path = "/path/to/my/other/chain/spec.json"
|
||||
cumulus_based = true
|
||||
evm_based = false
|
||||
|
||||
[[teyrchains.collators]]
|
||||
name = "mike"
|
||||
validator = true
|
||||
invulnerable = true
|
||||
bootnode = true
|
||||
balance = 5000000000
|
||||
|
||||
[[teyrchains.collators]]
|
||||
name = "georges"
|
||||
validator = false
|
||||
invulnerable = true
|
||||
bootnode = true
|
||||
balance = 0
|
||||
|
||||
[[teyrchains.collators]]
|
||||
name = "victor"
|
||||
validator = true
|
||||
invulnerable = false
|
||||
bootnode = true
|
||||
balance = 1000000000
|
||||
|
||||
[[hrmp_channels]]
|
||||
sender = 1000
|
||||
recipient = 2000
|
||||
max_capacity = 150
|
||||
max_message_size = 5000
|
||||
|
||||
[[hrmp_channels]]
|
||||
sender = 2000
|
||||
recipient = 1000
|
||||
max_capacity = 200
|
||||
max_message_size = 8000
|
||||
Vendored
+76
@@ -0,0 +1,76 @@
|
||||
[settings]
|
||||
timeout = 3600
|
||||
node_spawn_timeout = 600
|
||||
tear_down_on_failure = true
|
||||
|
||||
[relaychain]
|
||||
chain = "polkadot"
|
||||
default_command = "polkadot"
|
||||
default_image = "docker.io/parity/polkadot:latest"
|
||||
default_db_snapshot = "https://storage.com/path/to/db_snapshot.tgz"
|
||||
default_args = [
|
||||
"-name=value",
|
||||
"--flag",
|
||||
]
|
||||
|
||||
[relaychain.default_resources.requests]
|
||||
memory = "500M"
|
||||
cpu = "100000"
|
||||
|
||||
[relaychain.default_resources.limits]
|
||||
memory = "4000M"
|
||||
cpu = "10Gi"
|
||||
|
||||
[[relaychain.nodes]]
|
||||
name = "alice"
|
||||
validator = true
|
||||
invulnerable = true
|
||||
bootnode = true
|
||||
balance = 1000000000
|
||||
|
||||
[[relaychain.nodes]]
|
||||
name = "bob"
|
||||
image = "mycustomimage:latest"
|
||||
command = "my-custom-command"
|
||||
args = ["-myothername=value"]
|
||||
validator = true
|
||||
invulnerable = true
|
||||
bootnode = true
|
||||
balance = 2000000000000
|
||||
db_snapshot = "https://storage.com/path/to/other/db_snapshot.tgz"
|
||||
|
||||
[relaychain.nodes.resources.requests]
|
||||
memory = "250Mi"
|
||||
cpu = "1000"
|
||||
|
||||
[relaychain.nodes.resources.limits]
|
||||
memory = "2Gi"
|
||||
cpu = "5Gi"
|
||||
|
||||
[[teyrchains]]
|
||||
id = 1000
|
||||
chain = "myparachain"
|
||||
add_to_genesis = true
|
||||
balance = 2000000000000
|
||||
default_command = "my-default-command"
|
||||
default_image = "mydefaultimage:latest"
|
||||
default_db_snapshot = "https://storage.com/path/to/other_snapshot.tgz"
|
||||
chain_spec_path = "/path/to/my/chain/spec.json"
|
||||
cumulus_based = true
|
||||
evm_based = false
|
||||
|
||||
[[teyrchains.collators]]
|
||||
name = "john"
|
||||
image = "anotherimage:latest"
|
||||
command = "my-non-default-command"
|
||||
validator = true
|
||||
invulnerable = true
|
||||
bootnode = true
|
||||
balance = 5000000000
|
||||
|
||||
[[teyrchains.collators]]
|
||||
name = "charles"
|
||||
validator = false
|
||||
invulnerable = true
|
||||
bootnode = true
|
||||
balance = 0
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
[settings]
|
||||
timeout = 3600
|
||||
node_spawn_timeout = 600
|
||||
|
||||
[relaychain]
|
||||
chain = "rococo-local"
|
||||
default_command = "polkadot"
|
||||
default_image = "docker.io/parity/polkadot:latest"
|
||||
default_args = ["-lparachain=debug"]
|
||||
|
||||
[[relaychain.nodes]]
|
||||
name = "alice"
|
||||
validator = true
|
||||
invulnerable = true
|
||||
bootnode = false
|
||||
balance = 2000000000000
|
||||
|
||||
[[relaychain.nodes]]
|
||||
name = "bob"
|
||||
args = ["--database=paritydb-experimental"]
|
||||
validator = true
|
||||
invulnerable = false
|
||||
bootnode = true
|
||||
balance = 2000000000000
|
||||
|
||||
[[teyrchains]]
|
||||
id = 1000
|
||||
chain = "myparachain"
|
||||
onboard_as_teyrchain = false
|
||||
balance = 2000000000000
|
||||
default_db_snapshot = "https://storage.com/path/to/db_snapshot.tgz"
|
||||
chain_spec_path = "/path/to/my/chain/spec.json"
|
||||
cumulus_based = true
|
||||
|
||||
[teyrchains.collator]
|
||||
name = "john"
|
||||
validator = true
|
||||
invulnerable = true
|
||||
bootnode = true
|
||||
balance = 5000000000
|
||||
|
||||
[[teyrchains]]
|
||||
id = 1000
|
||||
chain = "myparachain"
|
||||
onboard_as_teyrchain = false
|
||||
balance = 2000000000000
|
||||
default_db_snapshot = "https://storage.com/path/to/db_snapshot.tgz"
|
||||
chain_spec_path = "/path/to/my/chain/spec.json"
|
||||
cumulus_based = true
|
||||
evm_based = true
|
||||
|
||||
[[teyrchains.collators]]
|
||||
name = "john"
|
||||
validator = true
|
||||
invulnerable = true
|
||||
bootnode = true
|
||||
balance = 5000000000
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
[relaychain]
|
||||
chain = "rococo-local"
|
||||
default_command = "polkadot"
|
||||
|
||||
[[relaychain.nodes]]
|
||||
name = "alice"
|
||||
|
||||
[[relaychain.nodes]]
|
||||
name = "bob"
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
[relaychain]
|
||||
chain = "rococo-local"
|
||||
default_command = "polkadot"
|
||||
wasm_override = "/some/path/runtime.wasm"
|
||||
|
||||
[[relaychain.nodes]]
|
||||
name = "alice"
|
||||
|
||||
[[relaychain.nodes]]
|
||||
name = "bob"
|
||||
|
||||
[[teyrchains]]
|
||||
id = 1000
|
||||
wasm_override = "https://some.com/runtime.wasm"
|
||||
|
||||
[teyrchains.collator]
|
||||
name = "john"
|
||||
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
[relaychain]
|
||||
default_command = "polkadot"
|
||||
chain_spec_path = "./rc.json"
|
||||
|
||||
[[relaychain.nodes]]
|
||||
name = "alice"
|
||||
validator = true
|
||||
rpc_port = 9944
|
||||
|
||||
[[relaychain.nodes]]
|
||||
name = "bob"
|
||||
validator = true
|
||||
rpc_port = 9945
|
||||
args = [
|
||||
"-lruntime::system=debug,runtime::session=trace,runtime::staking::ah-client=trace,runtime::ah-client=debug",
|
||||
]
|
||||
|
||||
[[teyrchains]]
|
||||
id = 1100
|
||||
chain_spec_path = "./parachain.json"
|
||||
|
||||
[teyrchains.collator]
|
||||
name = "charlie"
|
||||
rpc_port = 9946
|
||||
args = [
|
||||
"-lruntime::system=debug,runtime::multiblock-election=trace,runtime::staking=debug,runtime::staking::rc-client=trace,runtime::rc-client=debug",
|
||||
]
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
[settings]
|
||||
timeout = 3600
|
||||
node_spawn_timeout = 600
|
||||
|
||||
[relaychain]
|
||||
chain = "rococo-local"
|
||||
default_command = "polkadot"
|
||||
default_image = "docker.io/parity/polkadot:latest"
|
||||
default_args = ["-lparachain=debug"]
|
||||
|
||||
[[relaychain.nodes]]
|
||||
name = "alice"
|
||||
validator = true
|
||||
invulnerable = true
|
||||
bootnode = false
|
||||
balance = 2000000000000
|
||||
|
||||
[[relaychain.nodes]]
|
||||
name = "bob"
|
||||
args = ["--database=paritydb-experimental"]
|
||||
validator = true
|
||||
invulnerable = false
|
||||
bootnode = true
|
||||
balance = 2000000000000
|
||||
|
||||
[[teyrchains]]
|
||||
id = 1000
|
||||
chain = "myparachain"
|
||||
onboard_as_teyrchain = false
|
||||
balance = 2000000000000
|
||||
default_db_snapshot = "https://storage.com/path/to/db_snapshot.tgz"
|
||||
chain_spec_path = "/path/to/my/chain/spec.json"
|
||||
cumulus_based = true
|
||||
|
||||
[teyrchains.collator]
|
||||
name = "alice"
|
||||
validator = true
|
||||
invulnerable = true
|
||||
bootnode = true
|
||||
balance = 5000000000
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
[relaychain]
|
||||
chain = "rococo-local"
|
||||
default_command = "polkadot"
|
||||
raw_spec_override = "/some/path/raw_spec_override.json"
|
||||
|
||||
[[relaychain.nodes]]
|
||||
name = "alice"
|
||||
|
||||
[[relaychain.nodes]]
|
||||
name = "bob"
|
||||
|
||||
[[teyrchains]]
|
||||
id = 1000
|
||||
raw_spec_override = "https://some.com/raw_spec_override.json"
|
||||
|
||||
[teyrchains.collator]
|
||||
name = "john"
|
||||
Reference in New Issue
Block a user