Spawn test node process for integration tests (#252)

* Move tests to module in own file

* Add test-node-proc crate

* Move test node to main lib, fix compilation errors

* Fmt

* Attempted port scanning

* Use global static port to allocate open ports

* Register missing type sizes

* Fix port scanning and balances test

* Don't wait for blocks on insert_jey test

* Fmt

* Remove unused dependencies

* Download v3.0.0 rust binary

* Move substrate bin to home dir

* Add test instructions to README

* Figure out the $PATH

* Remove path echo

* Add check for missing substrate binary on the PATH

* @niklasad1 review: don't include client feature for tests
This commit is contained in:
Andrew Jones
2021-03-19 11:28:35 +00:00
committed by GitHub
parent 3b362fb072
commit bd885129f3
10 changed files with 484 additions and 201 deletions
+14 -8
View File
@@ -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::<TestRuntime, _>::new(AccountKeyring::Alice.pair());
let bob = PairSigner::<TestRuntime, _>::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::<TestRuntime, _>::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::<TestRuntime, _>::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::<TestRuntime>::new(sub, &decoder);
+5 -3
View File
@@ -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::<TestRuntime, _>::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::<TestRuntime, _>::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 {
+2 -183
View File
@@ -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<TestRuntime>, 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<TestRuntime>, 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::<system::AccountStore<_>>(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::<system::AccountStore<_>>(None).await.unwrap();
let mut i = 0;
while let Some(_) = iter.next().await.unwrap() {
i += 1;
}
assert_eq!(i, 4);
}
}
+5 -5
View File
@@ -157,7 +157,7 @@ pub enum TransactionStatus<Hash, BlockHash> {
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<HttpClient>),
#[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<HttpClient> for RpcClient {
}
}
#[cfg(any(feature = "client", test))]
#[cfg(feature = "client")]
impl From<SubxtClient> for RpcClient {
fn from(client: SubxtClient) -> Self {
RpcClient::Subxt(client)
+15
View File
@@ -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<T: Runtime>(
event_type_registry: &mut EventTypeRegistry<T>,
) {
// 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>("bool");
event_type_registry.register_type_size::<u8>("u8");
event_type_registry.register_type_size::<u16>("u16");
event_type_registry.register_type_size::<u32>("u32");
event_type_registry.register_type_size::<u64>("u64");
event_type_registry.register_type_size::<u128>("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<T: Runtime>(
event_type_registry.register_type_size::<[u8; 16]>("Kind");
event_type_registry.register_type_size::<u32>("AccountIndex");
event_type_registry.register_type_size::<u32>("AssetId");
event_type_registry.register_type_size::<u32>("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::<u32>("PropIndex");
event_type_registry.register_type_size::<u32>("ProposalIndex");
event_type_registry.register_type_size::<CLikeEnum>("ProxyType");
event_type_registry.register_type_size::<u32>("AuthorityIndex");
event_type_registry.register_type_size::<u32>("MemberCount");
event_type_registry.register_type_size::<u32>("RegistrarIndex");
event_type_registry.register_type_size::<u8>("VoteThreshold");
event_type_registry
.register_type_size::<(T::BlockNumber, u32)>("TaskAddress<BlockNumber>");
event_type_registry
.register_type_size::<(T::BlockNumber, u32)>("Timepoint<BlockNumber>");
event_type_registry.register_type_size::<AuthorityId>("AuthorityId");
event_type_registry.register_type_size::<AuthorityWeight>("AuthorityWeight");
+176
View File
@@ -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 <http://www.gnu.org/licenses/>.
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<TestRuntime> {
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::<TestRuntime>::build(SUBSTRATE_NODE_PATH)
.with_authority(key)
.scan_for_open_ports()
.spawn::<TestRuntime>()
.await;
proc.unwrap()
}
pub(crate) async fn test_node_process() -> TestNodeProcess<TestRuntime> {
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::<system::AccountStore<_>>(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::<system::AccountStore<_>>(None).await.unwrap();
let mut i = 0;
while let Some(_) = iter.next().await.unwrap() {
i += 1;
}
assert_eq!(i, 13);
}
+237
View File
@@ -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 <http://www.gnu.org/licenses/>.
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<R: Runtime> {
proc: process::Child,
client: Client<R>,
}
impl<R> Drop for TestNodeProcess<R>
where
R: Runtime,
{
fn drop(&mut self) {
let _ = self.kill();
}
}
impl<R> TestNodeProcess<R>
where
R: Runtime,
{
/// Construct a builder for spawning a test node process.
pub fn build<S>(program: S) -> TestNodeProcessBuilder
where
S: AsRef<OsStr> + 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<R> {
&self.client
}
}
/// Construct a test node process.
pub struct TestNodeProcessBuilder {
node_path: OsString,
authority: Option<AccountKeyring>,
scan_port_range: bool,
}
impl TestNodeProcessBuilder {
pub fn new<P>(node_path: P) -> TestNodeProcessBuilder
where
P: AsRef<OsStr>,
{
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<R>(&self) -> Result<TestNodeProcess<R>, 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::<R>::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
}
}
}