Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify bootstrap function arguments with default-able options #2078

Merged
merged 6 commits into from
Apr 22, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 12 additions & 16 deletions tools/integration-test/src/tests/client_expiration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ use std::thread::sleep;

use ibc::core::ics03_connection::connection::State as ConnectionState;
use ibc::core::ics04_channel::channel::State as ChannelState;
use ibc::core::ics04_channel::Version as ChannelVersion;
use ibc_relayer::config::default::connection_delay as default_connection_delay;
use ibc_relayer::config::{self, Config, ModeConfig};

use ibc_test_framework::bootstrap::binary::chain::ForeignClientBuilder;
use ibc_test_framework::bootstrap::binary::chain::bootstrap_foreign_client_pair;
use ibc_test_framework::bootstrap::binary::channel::{
bootstrap_channel_with_chains, bootstrap_channel_with_connection,
};
Expand Down Expand Up @@ -140,7 +138,7 @@ impl BinaryChainTest for ChannelExpirationTest {
let connection = {
let _refresh_tasks = spawn_refresh_client_tasks(&chains.foreign_clients)?;

bootstrap_connection(&chains.foreign_clients, default_connection_delay(), false)?
bootstrap_connection(&chains.foreign_clients, Default::default())?
};

wait_for_client_expiry();
Expand Down Expand Up @@ -214,9 +212,11 @@ impl BinaryChainTest for ChannelExpirationTest {
"Trying to create new channel and worker after previous connection worker failed"
);

let foreign_clients_2 = ForeignClientBuilder::new(&chains.handle_a, &chains.handle_b)
.pair()
.bootstrap()?;
let foreign_clients_2 = bootstrap_foreign_client_pair(
&chains.handle_a,
&chains.handle_b,
Default::default(),
)?;

// Need to spawn refresh client for new clients to make sure they don't expire

Expand Down Expand Up @@ -282,10 +282,8 @@ impl BinaryChainTest for PacketExpirationTest {
&chains,
&PortId::transfer(),
&PortId::transfer(),
Order::Unordered,
ChannelVersion::ics20(),
default_connection_delay(),
false,
Default::default(),
Default::default(),
)?
};

Expand Down Expand Up @@ -367,14 +365,14 @@ impl BinaryChainTest for CreateOnExpiredClientTest {
let connection = {
let _refresh_tasks = spawn_refresh_client_tasks(&chains.foreign_clients)?;

bootstrap_connection(&chains.foreign_clients, default_connection_delay(), false)?
bootstrap_connection(&chains.foreign_clients, Default::default())?
};

wait_for_client_expiry();

info!("trying to bootstrap connection after IBC client is expired");

let res = bootstrap_connection(&chains.foreign_clients, default_connection_delay(), false);
let res = bootstrap_connection(&chains.foreign_clients, Default::default());

match res {
Ok(_) => {
Expand All @@ -397,9 +395,7 @@ impl BinaryChainTest for CreateOnExpiredClientTest {
connection,
&DualTagged::new(&PortId::transfer()),
&DualTagged::new(&PortId::transfer()),
Order::Unordered,
ChannelVersion::ics20(),
false,
Default::default(),
);

match res {
Expand Down
289 changes: 88 additions & 201 deletions tools/test-framework/src/bootstrap/binary/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,223 +26,95 @@ use crate::types::tagged::*;
use crate::types::wallet::{TestWallets, Wallet};
use crate::util::random::random_u64_range;

/// A builder to bootstrap two relayer chain handles with connected
/// foreign clients.
pub struct Builder<'config> {
test_config: &'config TestConfig,
node_a: FullNode,
node_b: FullNode,
client_options_a_to_b: ClientOptions,
client_options_b_to_a: ClientOptions,
#[derive(Default)]
pub struct BootstrapClientOptions {
pub client_options_a_to_b: ClientOptions,
pub client_options_b_to_a: ClientOptions,
}

impl<'config> Builder<'config> {
/// Initializes the builder with two [`FullNode`] values representing two different
/// running full nodes, to bootstrap chain A and chain B respectively.
pub fn with_node_pair(
test_config: &'config TestConfig,
node_a: FullNode,
node_b: FullNode,
) -> Self {
Self {
test_config,
node_a,
node_b,
client_options_a_to_b: Default::default(),
client_options_b_to_a: Default::default(),
}
}
Comment on lines -39 to -54
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove the builder for a less convenient function API? This has the same purpose: make the test code more convenient to write in the simplest cases, while making customization possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the builder pattern is fine as long as it does not force the user to always use the pattern. There is a trade off between readability and writability. When I tried to use the new builder pattern, I found it much more difficult to understand, as I now have to jump between dozens of lines of code with mutation, side effects, and could potentially be executed non-linearly. In comparison, function argument passing have linear semantics, so I just need to read the few lines to immediately understand the semantics of the code.

Anyway, I re-introduce the builder pattern in aa336c7 with the pattern applied to the BootstrapClientOptions type instead. The builder methods are opt-in, with the Default trait or explicit field accessors available in case users want to opt out. This helps make it clear that there are only syntactic sugar methods and users only need to worry about side effects in the other functions.

Copy link
Contributor

@mzabaluev mzabaluev Apr 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mandatory config_modifier closure argument is also annoying. In the builder, I tried to simplify this by having two finalization methods: bootstrap as the basic case without the modifier closure, and bootstrap_with_config to provide the modifier. Passing the closure to the final build method also hints the user that the closure is invoked during the bootstrap process, not as part of the option setup.

Readability is subjective here. To me, it's much more intuitive to read the builder method chain where most of the parameters are supplied to a clearly named method, if they need to be customized at all, rather than trying to remember the meaning or the "dunno, use default" value of each of the anonymous arguments in a function call site. This is the rationale behind the clippy lint and the related API guideline. In this case it's not that bad, three to five parameters with distinct types, so let's go with this. These utilities are used by the test framework, not the tests themselves, so readability and convenience is less important here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the main point is that these are internal functions that are not meant to be called frequently. They act more as code fragments not as reusable modules. The higher level constructs like BinaryChainTest are the real abstraction that hides the complexity, so we only need to focus on providing clean interfaces there.

/// Bootstraps two relayer chain handles with connected foreign clients.
///
/// Returns a tuple consisting of the [`RelayerDriver`] and a
/// [`ConnectedChains`] object that contains the given
/// full nodes together with the corresponding two [`ChainHandle`]s and
/// [`ForeignClient`]s.
pub fn bootstrap_chains_with_full_nodes(
test_config: &TestConfig,
node_a: FullNode,
node_b: FullNode,
options: BootstrapClientOptions,
config_modifier: impl FnOnce(&mut Config),
) -> Result<
(
RelayerDriver,
ConnectedChains<impl ChainHandle, impl ChainHandle>,
),
Error,
> {
let mut config = Config::default();

/// Work similary to [`with_node_pair`][wnp], but bootstraps a
/// single chain to be connected with itself.
///
/// Self-connected chains are in fact allowed in IBC. Although we do not
/// have a clear use case for it yet, it is important to verify that
/// tests that pass with two connected chains should also pass with
/// self-connected chains.
///
/// [wnp]: Builder::with_node_pair
///
pub fn self_connected(test_config: &'config TestConfig, node: FullNode) -> Self {
let node1 = node.clone();
Self::with_node_pair(test_config, node, node1)
}
add_chain_config(&mut config, &node_a)?;
add_chain_config(&mut config, &node_b)?;

/// Overrides options for the foreign client connecting chain A to chain B.
pub fn client_options_a_to_b(mut self, options: ClientOptions) -> Self {
self.client_options_a_to_b = options;
self
}
config_modifier(&mut config);

/// Overrides options for the foreign client connecting chain B to chain A.
pub fn client_options_b_to_a(mut self, options: ClientOptions) -> Self {
self.client_options_b_to_a = options;
self
}
let config_path = test_config.chain_store_dir.join("relayer-config.toml");

/// Bootstraps two relayer chain handles with connected foreign clients.
///
/// Returns a tuple consisting of the [`RelayerDriver`] and a
/// [`ConnectedChains`] object that contains the given
/// full nodes together with the corresponding two [`ChainHandle`]s and
/// [`ForeignClient`]s.
pub fn bootstrap(
self,
) -> Result<
(
RelayerDriver,
ConnectedChains<impl ChainHandle, impl ChainHandle>,
),
Error,
> {
self.bootstrap_with_config(|_| {})
}
save_relayer_config(&config, &config_path)?;

/// Bootstraps two relayer chain handles with connected foreign clients.
///
/// Returns a tuple consisting of the [`RelayerDriver`] and a
/// [`ConnectedChains`] object that contains the given
/// full nodes together with the corresponding two [`ChainHandle`]s and
/// [`ForeignClient`]s.
///
/// This method gives the caller a way to modify the relayer configuration
/// that is pre-generated from the configurations of the full nodes.
pub fn bootstrap_with_config(
self,
config_modifier: impl FnOnce(&mut Config),
) -> Result<
(
RelayerDriver,
ConnectedChains<impl ChainHandle, impl ChainHandle>,
),
Error,
> {
let mut config = Config::default();

add_chain_config(&mut config, &self.node_a)?;
add_chain_config(&mut config, &self.node_b)?;

config_modifier(&mut config);

let config_path = self.test_config.chain_store_dir.join("relayer-config.toml");

save_relayer_config(&config, &config_path)?;

let registry = new_registry(config.clone());

// Pass in unique closure expressions `||{}` as the first argument so that
// the returned chains are considered different types by Rust.
// See [`spawn_chain_handle`] for more details.
let handle_a = spawn_chain_handle(|| {}, &registry, &self.node_a)?;
let handle_b = spawn_chain_handle(|| {}, &registry, &self.node_b)?;

if self.test_config.bootstrap_with_random_ids {
pad_client_ids(&handle_a, &handle_b)?;
pad_client_ids(&handle_b, &handle_a)?;
}

let foreign_clients = ForeignClientBuilder::new(&handle_a, &handle_b)
.client_options(self.client_options_a_to_b)
.pair()
.client_options(self.client_options_b_to_a)
.bootstrap()?;

let relayer = RelayerDriver {
config_path,
config,
registry,
hang_on_fail: self.test_config.hang_on_fail,
};

let chains = ConnectedChains::new(
handle_a,
handle_b,
MonoTagged::new(self.node_a),
MonoTagged::new(self.node_b),
foreign_clients,
);

Ok((relayer, chains))
}
}
let registry = new_registry(config.clone());

pub fn pad_client_ids<ChainA: ChainHandle, ChainB: ChainHandle>(
chain_a: &ChainA,
chain_b: &ChainB,
) -> Result<(), Error> {
let foreign_client =
ForeignClient::restore(ClientId::default(), chain_b.clone(), chain_a.clone());
// Pass in unique closure expressions `||{}` as the first argument so that
// the returned chains are considered different types by Rust.
// See [`spawn_chain_handle`] for more details.
let handle_a = spawn_chain_handle(|| {}, &registry, &node_a)?;
let handle_b = spawn_chain_handle(|| {}, &registry, &node_b)?;

for i in 0..random_u64_range(1, 6) {
debug!("creating new client id {} on chain {}", i + 1, chain_b.id());
foreign_client.build_create_client_and_send(Default::default())?;
if test_config.bootstrap_with_random_ids {
pad_client_ids(&handle_a, &handle_b)?;
pad_client_ids(&handle_b, &handle_a)?;
}

Ok(())
}

pub struct ForeignClientBuilder<'a, ChainA: ChainHandle, ChainB: ChainHandle> {
chain_a: &'a ChainA,
chain_b: &'a ChainB,
client_options: ClientOptions,
}

pub struct ForeignClientPairBuilder<'a, ChainA: ChainHandle, ChainB: ChainHandle> {
a_to_b: ForeignClientBuilder<'a, ChainA, ChainB>,
b_to_a_client_options: ClientOptions,
}

impl<'a, ChainA: ChainHandle, ChainB: ChainHandle> ForeignClientBuilder<'a, ChainA, ChainB> {
pub fn new(chain_a: &'a ChainA, chain_b: &'a ChainB) -> Self {
Self {
chain_a,
chain_b,
client_options: Default::default(),
}
}

pub fn client_options(mut self, settings: ClientOptions) -> Self {
self.client_options = settings;
self
}

/// Bootstrap a foreign client from `ChainA` to `ChainB`, i.e. the foreign
/// client collects information from `ChainA` and submits them as transactions
/// to `ChainB`.
///
/// The returned `ForeignClient` is tagged in contravariant ordering, i.e.
/// `ChainB` then `ChainB`, because `ForeignClient` takes the the destination
/// chain in the first position.
pub fn bootstrap(self) -> Result<ForeignClient<ChainB, ChainA>, Error> {
bootstrap_foreign_client(self.chain_a, self.chain_b, self.client_options)
}
let foreign_clients = bootstrap_foreign_client_pair(&handle_a, &handle_b, options)?;

let relayer = RelayerDriver {
config_path,
config,
registry,
hang_on_fail: test_config.hang_on_fail,
};

let chains = ConnectedChains::new(
handle_a,
handle_b,
MonoTagged::new(node_a),
MonoTagged::new(node_b),
foreign_clients,
);

/// Continues the builder composition for a pair of clients in both directions.
pub fn pair(self) -> ForeignClientPairBuilder<'a, ChainA, ChainB> {
ForeignClientPairBuilder {
a_to_b: self,
b_to_a_client_options: Default::default(),
}
}
Ok((relayer, chains))
}

impl<'a, ChainA: ChainHandle, ChainB: ChainHandle> ForeignClientPairBuilder<'a, ChainA, ChainB> {
/// Overrides the settings for a client in the reverse direction (B to A).
pub fn client_options(mut self, settings: ClientOptions) -> Self {
self.b_to_a_client_options = settings;
self
}

pub fn bootstrap(self) -> Result<ForeignClientPair<ChainA, ChainB>, Error> {
let chain_a = self.a_to_b.chain_a;
let chain_b = self.a_to_b.chain_b;
let client_a_to_b = bootstrap_foreign_client(chain_a, chain_b, self.a_to_b.client_options)?;
let client_b_to_a = bootstrap_foreign_client(chain_b, chain_a, self.b_to_a_client_options)?;
Ok(ForeignClientPair::new(client_a_to_b, client_b_to_a))
}
/// Bootstraps two relayer chain handles with connected foreign clients.
///
/// Returns a tuple consisting of the [`RelayerDriver`] and a
/// [`ConnectedChains`] object that contains the given
/// full nodes together with the corresponding two [`ChainHandle`]s and
/// [`ForeignClient`]s.
///
/// This method gives the caller a way to modify the relayer configuration
/// that is pre-generated from the configurations of the full nodes.
pub fn bootstrap_foreign_client_pair<ChainA: ChainHandle, ChainB: ChainHandle>(
chain_a: &ChainA,
chain_b: &ChainB,
options: BootstrapClientOptions,
) -> Result<ForeignClientPair<ChainA, ChainB>, Error> {
let client_a_to_b = bootstrap_foreign_client(chain_a, chain_b, options.client_options_a_to_b)?;
let client_b_to_a = bootstrap_foreign_client(chain_b, chain_a, options.client_options_b_to_a)?;
Ok(ForeignClientPair::new(client_a_to_b, client_b_to_a))
}

fn bootstrap_foreign_client<ChainA: ChainHandle, ChainB: ChainHandle>(
pub fn bootstrap_foreign_client<ChainA: ChainHandle, ChainB: ChainHandle>(
chain_a: &ChainA,
chain_b: &ChainB,
client_options: ClientOptions,
Expand All @@ -268,6 +140,21 @@ fn bootstrap_foreign_client<ChainA: ChainHandle, ChainB: ChainHandle>(
))
}

pub fn pad_client_ids<ChainA: ChainHandle, ChainB: ChainHandle>(
chain_a: &ChainA,
chain_b: &ChainB,
) -> Result<(), Error> {
let foreign_client =
ForeignClient::restore(ClientId::default(), chain_b.clone(), chain_a.clone());

for i in 0..random_u64_range(1, 6) {
debug!("creating new client id {} on chain {}", i + 1, chain_b.id());
foreign_client.build_create_client_and_send(Default::default())?;
}

Ok(())
}

/**
Spawn a new chain handle using the given [`SharedRegistry`] and
[`FullNode`].
Expand Down
Loading