-
Notifications
You must be signed in to change notification settings - Fork 85
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
Integration tests: fudge support for the generic environment #1588
Conversation
fa5d308
to
f65d330
Compare
f65d330
to
084d148
Compare
I wanted to expand this point from the description a bit more:
Basically, we have different behaviors for the same
|
380b817
to
c94c1ad
Compare
The PR with the proposed changes is done. I'm adding loan's tests to polish some minor part, but the main structure can be reviewed or receive feedback |
I would love to review this later today 🤟 |
) -> RelaychainBuilder<Self::RelayConstructApi, Self::RelayRuntime> { | ||
sp_tracing::enter_span!(sp_tracing::Level::INFO, "Relay - StartUp"); | ||
|
||
let code = Self::RELAY_CODE.unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer handling this and following unwraps or at least have some meaningful messages that can help our future selves in debugging this better ^^
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would say it's fine not to propagate the error here, because from a tester perspective, you always want a builder()
, if not, you're doing something wrong in using this API.
I think unwrap/expect are fine to be used when you want to force a precondition of the usage of the API. In production, we should not use them because of the security implications of reaching a unwrap
someday after some refactoring. However, having an unwrap
near the originator of the error helps debugging and for testing purposes, I think improves the feedback cycle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, I agree. I think we should have this consistent throughout the calls that we're doing here and maybe just use expect
with some meaningful messages?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I've restored the original expect()
messages from util/env.rs
file.
As a personal opinion, I usually try to reject expect()
because most cases, we end up with duplicated obvious descriptions of why it could not fail, and the descriptions makes the code more overwhelming. i.e: list.find(...).expect("Cannot fail, it must exist")
. Usually, Option
does not need an expect description because the option meaning implies absence regarding the action you call, and Result
does not need it because the panic message and error type explain the reason why it panicked in case of unexpected panic. For me, this is the case for 90% of unwrap/expect methods.
Of course, this is a personal point of view regarding this. Sure exist an opposite thought
pub trait CurrencyInfo { | ||
const ID: CurrencyId; | ||
const DECIMALS: u32; | ||
const UNIT: Balance = 10u128.pow(Self::DECIMALS); | ||
const SYMBOL: &'static str; | ||
const NAME: &'static str = Self::SYMBOL; | ||
const LOCATION: Option<xcm::VersionedMultiLocation> = None; | ||
const CUSTOM: CustomMetadata; | ||
const ED: Balance = 0; | ||
|
||
fn metadata() -> AssetMetadata<Balance, CustomMetadata> { | ||
AssetMetadata { | ||
decimals: Self::DECIMALS, | ||
name: Self::NAME.as_bytes().to_vec(), | ||
symbol: Self::SYMBOL.as_bytes().to_vec(), | ||
existential_deposit: Self::ED, | ||
location: None, | ||
additional: CustomMetadata { | ||
pool_currency: true, | ||
..Default::default() | ||
}, | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is experimental, to have good ergonomics later when testing something related to a currency, i.e (following comment):
Genesis::<T>::default() | ||
.add(genesis::balances(T::ExistentialDeposit::get() + FOR_FEES)) | ||
.add(genesis::assets(vec![Usd6::ID])) | ||
.add(genesis::tokens(vec![(Usd6::ID, Usd6::ED)])) | ||
.storage(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The above initializes a currency in orml_asset_registry
called Usd6
(6 decimals) with a metadata whose ED correctly matches the orml_tokens
. No more balance than ED
is given to all users (ALICE, BOB...) for that currency.
|
||
// In order to later close the epoch fastly, | ||
// we mofify here that requirement to significalty reduce the testing time. | ||
// The only way to do it is breaking the integration tests rules mutating | ||
// this state directly. | ||
pallet_pool_system::Pool::<T>::mutate(pool_id, |pool| { | ||
pool.as_mut().unwrap().parameters.min_epoch_time = POOL_MIN_EPOCH_TIME; | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mustermeiszer, my question about epoch time in slack came from this. If I call:
env.pass(Blocks::BySeconds(T::DefaultMinEpochTime::get()))
it takes around 10 min per test, even without fudge, only passing blocks to reach the point in time where I can actually closing the epoch.
If I modify the pool to set to the min bound, it takes around 60 seconds, but I think it's still not feasible if we expect to have a lot of loans tests (or any other thing that requires a funded pool).
The solution was to hardcode a really low value, but I don't like it because I should use something more high-level to deal with it. Doing this is really obscure.
This is ready for a final review & merge |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit. Otherwise locks sick!
pub fn balances<T: Runtime>(balance: Balance) -> impl GenesisBuild<T> { | ||
pallet_balances::GenesisConfig::<T> { | ||
balances: default_accounts() | ||
.into_iter() | ||
.map(|keyring| (keyring.id(), balance)) | ||
.collect(), | ||
} | ||
} | ||
|
||
pub fn tokens<T: Runtime>(values: Vec<(CurrencyId, Balance)>) -> impl GenesisBuild<T> { | ||
orml_tokens::GenesisConfig::<T> { | ||
balances: default_accounts() | ||
.into_iter() | ||
.map(|keyring| { | ||
values | ||
.clone() | ||
.into_iter() | ||
.map(|(curency_id, balance)| (keyring.id(), curency_id, balance)) | ||
.collect::<Vec<_>>() | ||
}) | ||
.flatten() | ||
.collect(), | ||
} | ||
} | ||
|
||
pub fn assets<T: Runtime>(currency_ids: Vec<CurrencyId>) -> impl GenesisBuild<T> { | ||
orml_asset_registry::GenesisConfig::<T> { | ||
assets: currency_ids | ||
.into_iter() | ||
.map(|currency_id| (currency_id, currency::find_metadata(currency_id).encode())) | ||
.collect(), | ||
last_asset_id: Default::default(), // It seems deprecated | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why redo what was already in the non generic part?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you mean, why not reuse the genesis functions from utils/genesis.rs
is because:
- I've tried, based on them, to make them more generic. The genesis methods from that module assume some currencies and initial values that maybe are not needed for some tests. These implementations are a bit more generic. Ideally, I would need to modify the others but doing it would make a lot of tests fail. Out of the scope of this PR, but we'll end up unifying them.
- Nit. Returning
impl GenesisBuild<T>
improves the ergonomics with theGenesis<T>
type (added in the previous PR). Passing a mutable ref to storage and later assimilating is a bit awkward, not sure why Substrate has chosen this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i.e: with this:
Genesis::<T>::default()
.add(genesis::balances(T::ExistentialDeposit::get() + FOR_FEES))
.add(genesis::assets(vec![Usd6::ID]))
.add(genesis::tokens(vec![(Usd6::ID, Usd6::ED)]))
.add(/* anything who implements GenesisBuild */)
.storage()
You know exactly how your initial state is for your current test without looking into the genesis functions. No more balances that can you see, nor more assets or tokens
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am against this. For integration tests I would rally like to see not setting everything up everywhere.
But let's see where we drift of from here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can always wrap a common initial state under some method and then call this method for a set of tests that requires that specific state. In the loan example, the common state is a function intended to be called from different loan tests that need the same pool state.
These genesis methods are a very low-level foundation you can compose to create your domain related genesis
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, each test domain could implement a GenesisBuild
where its initial state is built using the previous pieces, and later:
Genesis::<T>::default()
.add(genesis::loans_initial_state())
.storage()
or
Genesis::<T>::default()
.add(genesis::empty_pool())
.storage()
Things like those. Just drafting an idea of how it can evolve
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My point in favor of this is that I feel quite frustrated when a test I didn't write fails, and I need to dig into the first calls to know precisely what was the initial state it was built. And it was because of some global thing I didn't consider that was related to the failure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But let's see where we drift of from here
Thanks anyway for your "let's see" vote!
Description
This PR adds support for
fudge
to be used as a generic environment over #1583Fixes #1586
Moving from runtime environment to fudge environment
If you had a test as the following:
you can upgrade the test to use fudge as follows:
The
FudgeSupport
trait allows all features from fudge, andFudgeEnv
implements an environment that uses them under the hood.How does it work?
A fudge struct (
fudge::companion
struct) for each runtime implements the runtime agnostic traitFudgeHandle
that acts as a wrapper and unlocks all fudge features in a generic way. TheFudgeEnv
type implements theEnv
trait but makes use of theFudgeHandle
that is only available from the tests if theFudgeSupport
marker trait is added to theRuntime
.Progress
FudgeHandle
FudgeEnv
implementationsubmit()
behavior.RuntimeEnv
modifies the state aftersubmit()
and events can be read directly.FudgeEnv
required one block to modify the state aftersubmit()
and be able to read events.submit()
methods