diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8ae3ed743b..9914934777 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -23,6 +23,13 @@ jobs: components: rustfmt target: wasm32-unknown-unknown + - name: download-substrate + run: | + curl "https://releases.parity.io/substrate/x86_64-debian:stretch/v3.0.0/substrate/substrate" --output substrate --location + chmod +x ./substrate + mkdir -p ~/.local/bin + mv substrate ~/.local/bin + - name: fmt run: cargo fmt --all -- --check diff --git a/Cargo.toml b/Cargo.toml index 77c22ced10..8a619eedf1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,8 +58,7 @@ env_logger = "0.8.2" frame-system = "3.0.0" pallet-balances = "3.0.0" sp-keyring = "3.0.0" -substrate-subxt-client = { path = "client" } tempdir = "0.3.7" -test-node = { path = "test-node" } wabt = "0.10.0" +which = "4.0.2" assert_matches = "1.4.0" diff --git a/README.md b/README.md index 2114ccb05f..d44dff8efd 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,28 @@ A library to **sub**mit e**xt**rinsics to a [substrate](https://github.com/parit See [examples](./examples). +## Integration Testing + +Most tests require a running substrate node to communicate with. This is done by spawning an instance of the +substrate node per test. It requires an executable binary `substrate` at [`v3.0.0`](https://github.com/paritytech/substrate/releases/tag/v3.0.0) on your path. + +This can be done by downloading the prebuilt binary: + +```bash +curl "https://releases.parity.io/substrate/x86_64-debian:stretch/v3.0.0/substrate/substrate" --output substrate --location +chmod +x ./substrate +mv substrate ~/.local/bin +``` + +Or installed from source via cargo: + +```bash +cargo install --git https://github.com/paritytech/substrate node-cli --tag=v3.0.0 --force +``` + + + + **Alternatives** [substrate-api-client](https://github.com/scs/substrate-api-client) provides similar functionality. diff --git a/src/frame/balances.rs b/src/frame/balances.rs index 501eb8a4b8..aa487b4ac7 100644 --- a/src/frame/balances.rs +++ b/src/frame/balances.rs @@ -163,7 +163,7 @@ mod tests { subscription::EventSubscription, system::AccountStoreExt, tests::{ - test_client, + test_node_process, TestRuntime, }, }; @@ -179,7 +179,8 @@ mod tests { let alice = PairSigner::::new(AccountKeyring::Alice.pair()); let bob = PairSigner::::new(AccountKeyring::Bob.pair()); let bob_address = bob.account_id().clone().into(); - let (client, _) = test_client().await; + let test_node_proc = test_node_process().await; + let client = test_node_proc.client(); let alice_pre = client.account(alice.account_id(), None).await.unwrap(); let bob_pre = client.account(bob.account_id(), None).await.unwrap(); @@ -208,7 +209,8 @@ mod tests { #[async_std::test] async fn test_state_total_issuance() { env_logger::try_init().ok(); - let (client, _) = test_client().await; + let test_node_proc = test_node_process().await; + let client = test_node_proc.client(); let total_issuance = client.total_issuance(None).await.unwrap(); assert_ne!(total_issuance, 0); } @@ -216,7 +218,8 @@ mod tests { #[async_std::test] async fn test_state_read_free_balance() { env_logger::try_init().ok(); - let (client, _) = test_client().await; + let test_node_proc = test_node_process().await; + let client = test_node_proc.client(); let account = AccountKeyring::Alice.to_account_id(); let info = client.account(&account, None).await.unwrap(); assert_ne!(info.data.free, 0); @@ -270,14 +273,16 @@ mod tests { let alice_addr = alice.account_id().clone().into(); let hans = PairSigner::::new(Pair::generate().0); let hans_address = hans.account_id().clone().into(); - let (client, _) = test_client().await; + let test_node_proc = test_node_process().await; + let client = test_node_proc.client(); client - .transfer_and_watch(&alice, &hans_address, 100_000_000_000) + .transfer_and_watch(&alice, &hans_address, 100_000_000_000_000_000) .await .unwrap(); let res = client - .transfer_and_watch(&hans, &alice_addr, 100_000_000_000) + .transfer_and_watch(&hans, &alice_addr, 100_000_000_000_000_000) .await; + if let Err(Error::Runtime(RuntimeError::Module(error))) = res { let error2 = ModuleError { module: "Balances".into(), @@ -295,7 +300,8 @@ mod tests { let alice = PairSigner::::new(AccountKeyring::Alice.pair()); let bob = AccountKeyring::Bob.to_account_id(); let bob_addr = bob.clone().into(); - let (client, _) = test_client().await; + let test_node_proc = test_node_process().await; + let client = test_node_proc.client(); let sub = client.subscribe_events().await.unwrap(); let decoder = client.events_decoder(); let mut sub = EventSubscription::::new(sub, &decoder); diff --git a/src/frame/sudo.rs b/src/frame/sudo.rs index e0aee9f085..31fe409919 100644 --- a/src/frame/sudo.rs +++ b/src/frame/sudo.rs @@ -62,7 +62,7 @@ mod tests { extrinsic::PairSigner, frame::balances::TransferCall, tests::{ - test_client, + test_node_process, TestRuntime, }, }; @@ -73,7 +73,8 @@ mod tests { env_logger::try_init().ok(); let alice = PairSigner::::new(AccountKeyring::Alice.pair()); let bob = AccountKeyring::Bob.to_account_id().clone().into(); - let (client, _) = test_client().await; + let test_node_proc = test_node_process().await; + let client = test_node_proc.client(); let call = client .encode(TransferCall { @@ -97,7 +98,8 @@ mod tests { env_logger::try_init().ok(); let alice = PairSigner::::new(AccountKeyring::Alice.pair()); let bob = AccountKeyring::Bob.to_account_id().into(); - let (client, _) = test_client().await; + let test_node_proc = test_node_process().await; + let client = test_node_proc.client(); let call = client .encode(TransferCall { diff --git a/src/lib.rs b/src/lib.rs index 206dd662b1..c229e21f19 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -83,6 +83,8 @@ mod metadata; mod rpc; mod runtimes; mod subscription; +#[cfg(test)] +mod tests; pub use crate::{ error::{ @@ -656,186 +658,3 @@ impl codec::Encode for Encoded { self.0.to_owned() } } - -#[cfg(test)] -mod tests { - use super::*; - use sp_core::storage::{ - well_known_keys, - StorageKey, - }; - use sp_keyring::AccountKeyring; - use substrate_subxt_client::{ - DatabaseConfig, - KeystoreConfig, - Role, - SubxtClient, - SubxtClientConfig, - }; - use tempdir::TempDir; - - pub(crate) type TestRuntime = crate::NodeTemplateRuntime; - - pub(crate) async fn test_client_with( - key: AccountKeyring, - ) -> (Client, TempDir) { - env_logger::try_init().ok(); - let tmp = TempDir::new("subxt-").expect("failed to create tempdir"); - let config = SubxtClientConfig { - impl_name: "substrate-subxt-full-client", - impl_version: "0.0.1", - author: "substrate subxt", - copyright_start_year: 2020, - db: DatabaseConfig::RocksDb { - path: tmp.path().join("db"), - cache_size: 128, - }, - keystore: KeystoreConfig::Path { - path: tmp.path().join("keystore"), - password: None, - }, - chain_spec: test_node::chain_spec::development_config().unwrap(), - role: Role::Authority(key), - telemetry: None, - wasm_method: Default::default(), - }; - let client = ClientBuilder::new() - .set_client( - SubxtClient::from_config(config, test_node::service::new_full) - .expect("Error creating subxt client"), - ) - .set_page_size(3) - .build() - .await - .expect("Error creating client"); - (client, tmp) - } - pub(crate) async fn test_client() -> (Client, TempDir) { - test_client_with(AccountKeyring::Alice).await - } - - #[async_std::test] - async fn test_insert_key() { - // Bob is not an authority, so block production should be disabled. - let (client, _tmp) = test_client_with(AccountKeyring::Bob).await; - let mut blocks = client.subscribe_blocks().await.unwrap(); - // get the genesis block. - assert_eq!(blocks.next().await.unwrap().number, 0); - let public = AccountKeyring::Alice.public().as_array_ref().to_vec(); - client - .insert_key( - "aura".to_string(), - "//Alice".to_string(), - public.clone().into(), - ) - .await - .unwrap(); - assert!(client - .has_key(public.clone().into(), "aura".to_string()) - .await - .unwrap()); - // Alice is an authority, so blocks should be produced. - assert_eq!(blocks.next().await.unwrap().number, 1); - } - - #[async_std::test] - async fn test_tx_transfer_balance() { - let mut signer = PairSigner::new(AccountKeyring::Alice.pair()); - let dest = AccountKeyring::Bob.to_account_id().into(); - - let (client, _) = test_client().await; - let nonce = client - .account(&AccountKeyring::Alice.to_account_id(), None) - .await - .unwrap() - .nonce; - signer.set_nonce(nonce); - client - .submit( - balances::TransferCall { - to: &dest, - amount: 10_000, - }, - &signer, - ) - .await - .unwrap(); - - // check that nonce is handled correctly - signer.increment_nonce(); - client - .submit( - balances::TransferCall { - to: &dest, - amount: 10_000, - }, - &signer, - ) - .await - .unwrap(); - } - - #[async_std::test] - async fn test_getting_hash() { - let (client, _) = test_client().await; - client.block_hash(None).await.unwrap(); - } - - #[async_std::test] - async fn test_getting_block() { - let (client, _) = test_client().await; - let block_hash = client.block_hash(None).await.unwrap(); - client.block(block_hash).await.unwrap(); - } - - #[async_std::test] - async fn test_getting_read_proof() { - let (client, _) = test_client().await; - let block_hash = client.block_hash(None).await.unwrap(); - client - .read_proof( - vec![ - StorageKey(well_known_keys::HEAP_PAGES.to_vec()), - StorageKey(well_known_keys::EXTRINSIC_INDEX.to_vec()), - ], - block_hash, - ) - .await - .unwrap(); - } - - #[async_std::test] - async fn test_chain_subscribe_blocks() { - let (client, _) = test_client().await; - let mut blocks = client.subscribe_blocks().await.unwrap(); - blocks.next().await; - } - - #[async_std::test] - async fn test_chain_subscribe_finalized_blocks() { - let (client, _) = test_client().await; - let mut blocks = client.subscribe_finalized_blocks().await.unwrap(); - blocks.next().await; - } - - #[async_std::test] - async fn test_fetch_keys() { - let (client, _) = test_client().await; - let keys = client - .fetch_keys::>(4, None, None) - .await - .unwrap(); - assert_eq!(keys.len(), 4) - } - - #[async_std::test] - async fn test_iter() { - let (client, _) = test_client().await; - let mut iter = client.iter::>(None).await.unwrap(); - let mut i = 0; - while let Some(_) = iter.next().await.unwrap() { - i += 1; - } - assert_eq!(i, 4); - } -} diff --git a/src/rpc.rs b/src/rpc.rs index 20de9b1ac8..25705b3984 100644 --- a/src/rpc.rs +++ b/src/rpc.rs @@ -157,7 +157,7 @@ pub enum TransactionStatus { Invalid, } -#[cfg(any(feature = "client", test))] +#[cfg(feature = "client")] use substrate_subxt_client::SubxtClient; /// Rpc client wrapper. @@ -169,7 +169,7 @@ pub enum RpcClient { /// JSONRPC client HTTP transport. // NOTE: Arc because `HttpClient` is not clone. Http(Arc), - #[cfg(any(feature = "client", test))] + #[cfg(feature = "client")] /// Embedded substrate node. Subxt(SubxtClient), } @@ -186,7 +186,7 @@ impl RpcClient { inner.request(method, params).await.map_err(Into::into) } Self::Http(inner) => inner.request(method, params).await.map_err(Into::into), - #[cfg(any(feature = "client", test))] + #[cfg(feature = "client")] Self::Subxt(inner) => inner.request(method, params).await.map_err(Into::into), } } @@ -211,7 +211,7 @@ impl RpcClient { ) .into()) } - #[cfg(any(feature = "client", test))] + #[cfg(feature = "client")] Self::Subxt(inner) => { inner .subscribe(subscribe_method, params, unsubscribe_method) @@ -234,7 +234,7 @@ impl From for RpcClient { } } -#[cfg(any(feature = "client", test))] +#[cfg(feature = "client")] impl From for RpcClient { fn from(client: SubxtClient) -> Self { RpcClient::Subxt(client) diff --git a/src/runtimes.rs b/src/runtimes.rs index fdb5476054..985b14d364 100644 --- a/src/runtimes.rs +++ b/src/runtimes.rs @@ -203,6 +203,7 @@ impl Runtime for DefaultNodeRuntime { event_type_registry.with_system(); event_type_registry.with_balances(); event_type_registry.with_session(); + event_type_registry.with_staking(); event_type_registry.with_contracts(); event_type_registry.with_sudo(); register_default_type_sizes(event_type_registry); @@ -376,14 +377,20 @@ pub type AuthorityList = Vec<(AuthorityId, AuthorityWeight)>; pub fn register_default_type_sizes( event_type_registry: &mut EventTypeRegistry, ) { + // for types which have all variants with no data, the size is just the index byte. + type CLikeEnum = u8; + // primitives event_type_registry.register_type_size::("bool"); event_type_registry.register_type_size::("u8"); + event_type_registry.register_type_size::("u16"); event_type_registry.register_type_size::("u32"); event_type_registry.register_type_size::("u64"); event_type_registry.register_type_size::("u128"); event_type_registry.register_type_size::<()>("PhantomData"); + event_type_registry + .register_type_size::<()>("sp_std::marker::PhantomData<(AccountId, Event)>"); // frame_support types event_type_registry @@ -400,14 +407,22 @@ pub fn register_default_type_sizes( event_type_registry.register_type_size::<[u8; 16]>("Kind"); event_type_registry.register_type_size::("AccountIndex"); + event_type_registry.register_type_size::("AssetId"); + event_type_registry.register_type_size::("BountyIndex"); + event_type_registry.register_type_size::<(u8, u8)>("CallIndex"); + event_type_registry.register_type_size::<[u8; 32]>("CallHash"); event_type_registry.register_type_size::("PropIndex"); event_type_registry.register_type_size::("ProposalIndex"); + event_type_registry.register_type_size::("ProxyType"); event_type_registry.register_type_size::("AuthorityIndex"); event_type_registry.register_type_size::("MemberCount"); + event_type_registry.register_type_size::("RegistrarIndex"); event_type_registry.register_type_size::("VoteThreshold"); event_type_registry .register_type_size::<(T::BlockNumber, u32)>("TaskAddress"); + event_type_registry + .register_type_size::<(T::BlockNumber, u32)>("Timepoint"); event_type_registry.register_type_size::("AuthorityId"); event_type_registry.register_type_size::("AuthorityWeight"); diff --git a/src/tests/mod.rs b/src/tests/mod.rs new file mode 100644 index 0000000000..8681a904e0 --- /dev/null +++ b/src/tests/mod.rs @@ -0,0 +1,176 @@ +// Copyright 2019-2021 Parity Technologies (UK) Ltd. +// This file is part of substrate-subxt. +// +// subxt is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// subxt is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with substrate-subxt. If not, see . + +mod node_proc; + +use super::*; +use node_proc::TestNodeProcess; +use sp_core::storage::{ + well_known_keys, + StorageKey, +}; +use sp_keyring::AccountKeyring; + +/// substrate node should be installed on the $PATH +const SUBSTRATE_NODE_PATH: &str = "substrate"; + +pub(crate) type TestRuntime = crate::DefaultNodeRuntime; + +pub(crate) async fn test_node_process_with( + key: AccountKeyring, +) -> TestNodeProcess { + if which::which(SUBSTRATE_NODE_PATH).is_err() { + panic!("A substrate binary should be installed on your path for integration tests. See https://github.com/paritytech/substrate-subxt/tree/master#integration-testing") + } + + let proc = TestNodeProcess::::build(SUBSTRATE_NODE_PATH) + .with_authority(key) + .scan_for_open_ports() + .spawn::() + .await; + proc.unwrap() +} + +pub(crate) async fn test_node_process() -> TestNodeProcess { + test_node_process_with(AccountKeyring::Alice).await +} + +#[async_std::test] +async fn test_insert_key() { + let test_node_process = test_node_process_with(AccountKeyring::Bob).await; + let client = test_node_process.client(); + let public = AccountKeyring::Alice.public().as_array_ref().to_vec(); + client + .insert_key( + "aura".to_string(), + "//Alice".to_string(), + public.clone().into(), + ) + .await + .unwrap(); + assert!(client + .has_key(public.clone().into(), "aura".to_string()) + .await + .unwrap()); +} + +#[async_std::test] +async fn test_tx_transfer_balance() { + let mut signer = PairSigner::new(AccountKeyring::Alice.pair()); + let dest = AccountKeyring::Bob.to_account_id().into(); + + let node_process = test_node_process().await; + let client = node_process.client(); + let nonce = client + .account(&AccountKeyring::Alice.to_account_id(), None) + .await + .unwrap() + .nonce; + signer.set_nonce(nonce); + client + .submit( + balances::TransferCall { + to: &dest, + amount: 10_000, + }, + &signer, + ) + .await + .unwrap(); + + // check that nonce is handled correctly + signer.increment_nonce(); + client + .submit( + balances::TransferCall { + to: &dest, + amount: 10_000, + }, + &signer, + ) + .await + .unwrap(); +} + +#[async_std::test] +async fn test_getting_hash() { + let node_process = test_node_process().await; + node_process.client().block_hash(None).await.unwrap(); +} + +#[async_std::test] +async fn test_getting_block() { + let node_process = test_node_process().await; + let client = node_process.client(); + let block_hash = client.block_hash(None).await.unwrap(); + client.block(block_hash).await.unwrap(); +} + +#[async_std::test] +async fn test_getting_read_proof() { + let node_process = test_node_process().await; + let client = node_process.client(); + let block_hash = client.block_hash(None).await.unwrap(); + client + .read_proof( + vec![ + StorageKey(well_known_keys::HEAP_PAGES.to_vec()), + StorageKey(well_known_keys::EXTRINSIC_INDEX.to_vec()), + ], + block_hash, + ) + .await + .unwrap(); +} + +#[async_std::test] +async fn test_chain_subscribe_blocks() { + let node_process = test_node_process().await; + let client = node_process.client(); + let mut blocks = client.subscribe_blocks().await.unwrap(); + blocks.next().await; +} + +#[async_std::test] +async fn test_chain_subscribe_finalized_blocks() { + let node_process = test_node_process().await; + let client = node_process.client(); + let mut blocks = client.subscribe_finalized_blocks().await.unwrap(); + blocks.next().await; +} + +#[async_std::test] +async fn test_fetch_keys() { + let node_process = test_node_process().await; + let client = node_process.client(); + let keys = client + .fetch_keys::>(4, None, None) + .await + .unwrap(); + assert_eq!(keys.len(), 4) +} + +#[async_std::test] +async fn test_iter() { + let node_process = test_node_process().await; + let client = node_process.client(); + let mut iter = client.iter::>(None).await.unwrap(); + let mut i = 0; + while let Some(_) = iter.next().await.unwrap() { + i += 1; + } + assert_eq!(i, 13); +} diff --git a/src/tests/node_proc.rs b/src/tests/node_proc.rs new file mode 100644 index 0000000000..9f33d6985c --- /dev/null +++ b/src/tests/node_proc.rs @@ -0,0 +1,237 @@ +// Copyright 2019-2021 Parity Technologies (UK) Ltd. +// This file is part of substrate-subxt. +// +// subxt is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// subxt is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with substrate-subxt. If not, see . + +use crate::{ + Client, + ClientBuilder, + Runtime, +}; +use sp_keyring::AccountKeyring; +use std::{ + ffi::{ + OsStr, + OsString, + }, + net::TcpListener, + process, + sync::atomic::{ + AtomicU16, + Ordering, + }, + thread, + time, +}; + +/// Spawn a local substrate node for testing subxt. +pub struct TestNodeProcess { + proc: process::Child, + client: Client, +} + +impl Drop for TestNodeProcess +where + R: Runtime, +{ + fn drop(&mut self) { + let _ = self.kill(); + } +} + +impl TestNodeProcess +where + R: Runtime, +{ + /// Construct a builder for spawning a test node process. + pub fn build(program: S) -> TestNodeProcessBuilder + where + S: AsRef + Clone, + { + TestNodeProcessBuilder::new(program) + } + + /// Attempt to kill the running substrate process. + pub fn kill(&mut self) -> Result<(), String> { + log::info!("Killing contracts node process {}", self.proc.id()); + if let Err(err) = self.proc.kill() { + let err = format!( + "Error killing contracts node process {}: {}", + self.proc.id(), + err + ); + log::error!("{}", err); + return Err(err.into()) + } + Ok(()) + } + + /// Returns the subxt client connected to the running node. + pub fn client(&self) -> &Client { + &self.client + } +} + +/// Construct a test node process. +pub struct TestNodeProcessBuilder { + node_path: OsString, + authority: Option, + scan_port_range: bool, +} + +impl TestNodeProcessBuilder { + pub fn new

(node_path: P) -> TestNodeProcessBuilder + where + P: AsRef, + { + Self { + node_path: node_path.as_ref().into(), + authority: None, + scan_port_range: false, + } + } + + /// Set the authority dev account for a node in validator mode e.g. --alice. + pub fn with_authority(&mut self, account: AccountKeyring) -> &mut Self { + self.authority = Some(account); + self + } + + /// Enable port scanning to scan for open ports. + /// + /// Allows spawning multiple node instances for tests to run in parallel. + pub fn scan_for_open_ports(&mut self) -> &mut Self { + self.scan_port_range = true; + self + } + + /// Spawn the substrate node at the given path, and wait for rpc to be initialized. + pub async fn spawn(&self) -> Result, String> + where + R: Runtime, + { + let mut cmd = process::Command::new(&self.node_path); + cmd.env("RUST_LOG", "error").arg("--dev").arg("--tmp"); + + if let Some(authority) = self.authority { + let authority = format!("{:?}", authority); + let arg = format!("--{}", authority.as_str().to_lowercase()); + cmd.arg(arg); + } + + let ws_port = if self.scan_port_range { + let (p2p_port, http_port, ws_port) = next_open_port() + .ok_or("No available ports in the given port range".to_owned())?; + + cmd.arg(format!("--port={}", p2p_port)); + cmd.arg(format!("--rpc-port={}", http_port)); + cmd.arg(format!("--ws-port={}", ws_port)); + ws_port + } else { + // the default Websockets port + 9944 + }; + + let ws_url = format!("ws://127.0.0.1:{}", ws_port); + + let mut proc = cmd.spawn().map_err(|e| { + format!( + "Error spawning substrate node '{}': {}", + self.node_path.to_string_lossy(), + e + ) + })?; + // wait for rpc to be initialized + const MAX_ATTEMPTS: u32 = 10; + let mut attempts = 1; + let client = loop { + thread::sleep(time::Duration::from_secs(1)); + log::info!( + "Connecting to contracts enabled node, attempt {}/{}", + attempts, + MAX_ATTEMPTS + ); + let result = ClientBuilder::::new() + .set_url(ws_url.clone()) + .build() + .await; + match result { + Ok(client) => break Ok(client), + Err(crate::Error::MissingTypeSizes(e)) => { + break Err(crate::Error::MissingTypeSizes(e)) + } + Err(err) => { + if attempts < MAX_ATTEMPTS { + attempts += 1; + continue + } + break Err(err) + } + } + }; + match client { + Ok(client) => Ok(TestNodeProcess { proc, client }), + Err(err) => { + let err = format!( + "Failed to connect to node rpc at {} after {} attempts: {}", + ws_url, attempts, err + ); + log::error!("{}", err); + proc.kill().map_err(|e| { + format!("Error killing substrate process '{}': {}", proc.id(), e) + })?; + Err(err.into()) + } + } + } +} + +/// The start of the port range to scan. +const START_PORT: u16 = 9900; +/// The end of the port range to scan. +const END_PORT: u16 = 10000; +/// The maximum number of ports to scan before giving up. +const MAX_PORTS: u16 = 1000; +/// Next available unclaimed port for test node endpoints. +static PORT: AtomicU16 = AtomicU16::new(START_PORT); + +/// Returns the next set of 3 open ports. +/// +/// Returns None if there are not 3 open ports available. +fn next_open_port() -> Option<(u16, u16, u16)> { + let mut ports = Vec::new(); + let mut ports_scanned = 0u16; + loop { + let _ = PORT.compare_exchange( + END_PORT, + START_PORT, + Ordering::SeqCst, + Ordering::SeqCst, + ); + let next = PORT.fetch_add(1, Ordering::SeqCst); + match TcpListener::bind(("0.0.0.0", next)) { + Ok(_) => { + ports.push(next); + if ports.len() == 3 { + return Some((ports[0], ports[1], ports[2])) + } + } + Err(_) => (), + } + ports_scanned += 1; + if ports_scanned == MAX_PORTS { + return None + } + } +}