-
Notifications
You must be signed in to change notification settings - Fork 353
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
Refactor of cw1-whitelist unit tests #343
Conversation
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.
Nice initiative and you seem to be finding your way around the repo quite well.
Added some comments for a few requested changes, otherwise looks good.
@@ -130,7 +130,7 @@ where | |||
pub fn check_staking_permissions( | |||
staking_msg: &StakingMsg, | |||
permissions: Permissions, | |||
) -> Result<bool, ContractError> { | |||
) -> Result<(), ContractError> { |
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 return a bool here if we only have Ok(true)
(never Ok(false)
).
It suggests there are 3 possible cases to handle: Ok(true)
, Ok(false)
, Err(_)
, when there are only 2: Ok(_)
and Err(_)
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.
Yes, it is exactly why I changed it from bool to ZST - it was hurting my eyes (and also optimizer - in case when one type of Result is ZST, the Result itself becomes transparent).
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 returned Result<(), ContractError>
as it captured more failure cases than 1 (a bool
would just capture one success and one failure case).
We have 1 success case and 4 failure cases.
Ahhh.... I misread the diff... I thought you made the bool. You removed it. 👀
Very good change
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.
👍🏼
} | ||
|
||
pub fn check_distribution_permissions( | ||
distribution_msg: &DistributionMsg, | ||
permissions: Permissions, | ||
) -> Result<bool, ContractError> { |
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.
Ditto
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.
Maybe you want to rename to assert_*_permissions
? Then the return type would make more sense
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 mean I find naming "check_" functions returning Result
on failure handy (if I would like to return bool
then I would call it more like question, let say - has_distribution_permissions
). assert_*
suggests, panic on failure from my POV (which basically happens here in one branch which I don't like - I would prefer debug assertion to check invalid branch as prerequirement, but on release I would just return error 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.
Nevermind, remove this suggestion. I misread the diff and the comments up here are wrong.
Your way is better (and what I was arguing for)
&initial_allowances, | ||
&initial_expirations, | ||
); | ||
let Suite { deps, .. } = SuiteConfig { |
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 see the idea of the Suite to handle many things more clearly.
Somehow the usage seems more verbose than the older setup code.
I wonder if it is possible to make usage more compact. With eg. 2 helpers:
Spender::with_allowance
and Spender::with_expiring_allowance
. That would reduce those 4-5 line struct inits into 1 line, and then the usage of the new Suite would really shine
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 actually thinking about this - this is very generic proposal, but suggestions are very much welcome. I even considered to split initialization and early execution, adding utility for executes, to achieve something like:
let Suite { deps, owner_info: owner, .. } = SuiteConfig {
// Only minimal config, or maybe just `Suite::init(...)` with args required to setup properly
}
.init()
.execute_increase_allowance(&owner, SENDER1, vec![coin(1111, TOKEN)], None)
.execute_increase_allowance(&owner, SENDER2, vec![coin(2222, TOKEN)], None);
This would benefit also redability of test logic imho. There can be argument, that this thing hides what is actually called (the execute
itself), but wrapping it in init hides it even more, so I am ok with that.
}, | ||
]; | ||
|
||
assert_sorted_eq!( |
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 like the check here to sort before compare.
/// | ||
/// This is implemented as a macro instead of function to throw panic in the place of macro | ||
/// usage instead of from function called inside test. | ||
macro_rules! assert_sorted_eq { |
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.
Interesting macro trick.
I write a number of helper functions and always re-run with RUST_BACKTRACE=1
when they fail to find the real location. This does seem more ergonomic.
We need to reconsider the macro-resistance in this codebase.
&initial_allowances, | ||
&initial_expirations, | ||
); | ||
// let's try pagination. |
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.
Nice comment
For some reason I cannot reply to this, but I will inline it:
I am happy adding all kinds of magic helpers to SuiteConfig / init. Anything to build up the Suite. This is just to set up initial state, we trust that code works. The code that is later being tested (execute), I would like to explicitly call. |
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.
Misread. I agree with the points on top.
Only comments relate to test setup helpers
@@ -130,7 +130,7 @@ where | |||
pub fn check_staking_permissions( | |||
staking_msg: &StakingMsg, | |||
permissions: Permissions, | |||
) -> Result<bool, ContractError> { | |||
) -> Result<(), ContractError> { |
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 returned Result<(), ContractError>
as it captured more failure cases than 1 (a bool
would just capture one success and one failure case).
We have 1 success case and 4 failure cases.
Ahhh.... I misread the diff... I thought you made the bool. You removed it. 👀
Very good change
} | ||
|
||
pub fn check_distribution_permissions( | ||
distribution_msg: &DistributionMsg, | ||
permissions: Permissions, | ||
) -> Result<bool, ContractError> { |
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.
Nevermind, remove this suggestion. I misread the diff and the comments up here are wrong.
Your way is better (and what I was arguing for)
@@ -149,13 +149,13 @@ pub fn check_staking_permissions( | |||
} | |||
s => panic!("Unsupported staking message: {:?}", s), |
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 should return Err().
Good eye
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 like these test helper structs / macros.
What we miss in being able to reference existing variables in test assertions, we gain in clarity and conciseness. Good work.
@@ -130,7 +130,7 @@ where | |||
pub fn check_staking_permissions( | |||
staking_msg: &StakingMsg, | |||
permissions: Permissions, | |||
) -> Result<bool, ContractError> { | |||
) -> Result<(), ContractError> { |
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.
👍🏼
assert_sorted_eq!( | ||
expected, | ||
[batch1, batch2].concat(), | ||
|l: &AllowanceInfo, r: &AllowanceInfo| l.spender.cmp(&r.spender) |
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.
Nice trick relying on PartialEq
for sorting, instead of requiring Ord
here.
I understand it would make sense to just add Ord
(and Eq
) to AllowanceInfo
, and simplify this.
As that can require adding them to other stuff, including some structs in cosmwasm-std, I would say, let's create an issue to do just this; at least for cosmwasm-std. And then we can revisit 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.
Defienetely implementing Ord
for AllowanceInfo
does not make sense - Ord
is for types with reasonable sorting, and I find implementing it for this kind of type a missuse. Note that even standard library doesn't implement sort for types like floats (even if you could technically provide implementation which would somehow properly sort it). Actually it is questionable if there is proper way to pick proper key for comparison - is this anyhow intuitive that allowance with sender earlier in alphabet is in any way smaller than the other one?
My personal opinion is, that even if this adds some code, it avoids any confusion how things are sorted (and why). I think about two possible simplifications:
- try to write macro in the way, that it would be more elision friendly so type hints are not needed (actually type hints are half the length of the closure!) - unfortunatelly I have no idea how to approach this
- extract the comparison function to
AllowanceInfo
, but not as an implementation ofOrd
, but rather testing purpose method likeAllowanceInfo::cmp_by_spender
- actually I like the idea
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.
2. extract the comparison function to
AllowanceInfo
, but not as an implementation ofOrd
, but rather testing purpose method likeAllowanceInfo::cmp_by_spender
- actually I like the idea
Sounds good. That, and implementing these helpers only for tests. That could include deriving something like Ord
, which does not make sense, or adds unnecessary complexity, only for tests using conditional compilation.
836160a
to
90285f5
Compare
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.
Very good stuff! Do you want to take it out of draft?
I made a few minor points, but the code looks great. The only concern I raise is two test cases are marked ignore
. If you aren't sure of the expected conditions to finish them, please add a comment on them and either Mauro or I can help.
Other than that, I'd approve it.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] | ||
pub struct PermissionsInfo { | ||
pub spender: String, | ||
pub permissions: Permissions, | ||
} | ||
|
||
#[cfg(any(test, feature = "test-utils"))] |
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.
Nice feature flag for imports
|
||
#[test] | ||
fn query() { | ||
let Suite { deps, .. } = SuiteConfig::new() |
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'm loving the API design here.
Super concise and flexible. The builder-pattern SuiteConfig is working out very well. Much better than a number of setup functions with different args... so much more power here.
self | ||
} | ||
|
||
fn expire_allowances(mut self, spender: &'static str, expires: Expiration) -> Self { |
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.
Okay, so these are different calls that can be combined.
with_allowance(SPENDER2, ..).expire_allowances(SPENDER2, ..)
works.
And expire_allowances(SPENDER2, ..).with_allowance(SPENDER2, ..)
should also work, right?
That is a very nice API avoiding N^2 init function issues. I would love a test case (on SuiteConfig) showing this works regardless of ordering. But design is 🥇
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 personally don't like to test this simple functions as it turns out to be testing language instead of logic, but possibly it could be tested. The order should not matter as it just sets state, but making API so it matters is obviously possible, I just don't see a point to complicate testing utilities.
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 there is a bug in that testing utilities, then some test case below will surely fail.
I would only write them if a test fails unexpectedly and it seems to be a util bug.
}, | ||
]; | ||
|
||
assert_sorted_eq!( |
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 concat (after we paginate through, we get all the values exactly once) is a nice test-case.
Cleaner to read and more stable if we change any implementation details, but captures business requirements
assert_eq!(rsp.data, None); | ||
|
||
assert_eq!( | ||
query_all_allowances(deps.as_ref(), None, None) |
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.
since there is only one SPENDER here, why not use query_allowance
?
It seems like it would be a bit simpler.
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.
Because as I am touching allowance, I want to be sure that only my specific allowance changed, and no side effect occurred. It is surprisingly almost the same (just one encapsulation more), and more generic test.
balance: NativeBalance(vec![allow1.clone(), allow2.clone()]), | ||
expires: expires_height, | ||
#[test] | ||
#[ignore] |
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 ignore? does this fail?
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.
Yes it fails. I am pretty sure this test is correct (but double check if I understand logic), and it should be implemented, and then test would be "unignored".
balance: NativeBalance(vec![allow1.clone()]), | ||
expires: expires_never, | ||
#[test] | ||
#[ignore] |
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.
ditto
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.
Same as above
Co-authored-by: Mauro Lacy <maurolacy@users.noreply.github.com>
Co-authored-by: Mauro Lacy <maurolacy@users.noreply.github.com>
Co-authored-by: Mauro Lacy <maurolacy@users.noreply.github.com>
Co-authored-by: DzikiChrzan <dzikichrzan@tuta.io>
I just removed draft flag. One thing I wanted to have before merging is getting rid of |
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.
Looks good to me.
Let's merge it and I can inspect the item about independence when I have a bit of space and comment on that. (And then have a smaller PR to review there).
Maybe you can pull that out into another mod (mod old_tests
) before merging, so it is clear which are using which util functions? Or just leave it as is, it should be clear enough what is missing still
As there is a request to create additional UTs for some contracts: #105, I went through the whitelist contract UT and found that kind of problem with adding many tests there is repetitive boilerplate in every test, which is not actually easy to read. Also because of some edge cases, some test may be actually incorrect (eg. if in future some items would be returned in different order because of any reason, UTs would start failing - while ordering in many cases shouldn't matter).
Therefore I came up with proposal to kind of unify the UTs structure to have visible setup => execute => verify structure, with - what is important - clear setup, so it is easier to figure out what is the test about.
Additional changes I am considering to make execution and possibly verify part simpler is to add
execute
/query
methods directly onSuite
(query via router), so thedeps
andmock_env
are hidden. Additionally it would make testing queries through whole ecosystem, not just locally which might be more complete.At the end I think, that the
Suite
structure could be partially generalized and extracted tomulti-test
, to encourage having clean an uniform test through the smart contracts.