-
Notifications
You must be signed in to change notification settings - Fork 15
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
feature request: Add support for actions without conditions. #22
Comments
@mds1 should be supported in https://crates.io/crates/bulloak/0.4.3! |
Amazing! Testing it out now and have two comments. Given:
Then run pragma solidity 0.8.0;
contract CreateXTest {
function test_ItShouldNeverRevert.() external {
// It should never revert.
}
function test_ItShouldMatchTheOutputOfAHigh_levelHash.() external {
// It should match the output of a high-level hash.
}
} So two observations:
pragma solidity 0.8.0;
contract MyContractTest {
function test_Requirements() external {
// It should never revert.
// It should match the output of a high-level hash.
}
} |
Ah, yes, I need to handle the period as well, good catch! Fixed in https://crates.io/crates/bulloak/0.4.4!
Yes, after thinking about it for a bit, I decided to go with 1 test per action (#26 (comment)). Even though this is inconsistent with how actions behave when they have parent conditions, I think it is less surprising, since actions without conditions suggest unrelated function invariants. That is, if they relate somehow, then they would be children of a condition. So, it is best to separate the test functions. Wdyt? For the tree above, the output would be: contract MyContractTest {
function test_ShouldNeverRevert() external {
// It should never revert.
}
function test_ShouldMatchTheOutputOfAHigh_levelHash() external {
// It should match the output of a high-level hash.
}
} |
Interesting, I'd argue it's actually more surprising, since now the structure of your tests depends on the function being tested: if it's a simple branchless method you have one test per requirement, and if it's more complex you have one test per set of preconditions. So now the test architecture also changes as the function being tested gets more complex, even if you just add a single
A simple counterexample in the In the linked PR:
is an interesting edge case. My reaction is this should not be allowed: The presence of "when something happens" implies the inverse of that should contain the other two conditions Might be good to pull @PaulRBerg in here for an additional opinion on the |
I'm not sure I fully understand since I believe the structure of the tests depends on the function being tested in the previous implementation. Let's say we start with:
Which gets us: contract MyContractTest {
function test_ShouldNeverRevert() external {
// It should never revert.
}
function test_ShouldMatchTheOutputOfAHigh_levelHash() external {
// It should match the output of a high-level hash.
}
} And then we decide to update the function a bit to handle some precondition, which we now describe in our
with output: pragma solidity 0.8.0;
contract MyContractTest {
function test_ShouldMatchTheOutputOfAHigh_levelHash() external {
// It should match the output of a high-level hash.
}
function test_ShouldNeverRevert() external {
// It should never revert.
}
modifier whenSomePreconditionOccurs() {
_;
}
function test_WhenSomePreconditionOccurs()
external
whenSomePreconditionOccurs
{
// it should do an action.
// it should do another action.
}
} The hypothesis is that the output above is more surprising than: pragma solidity 0.8.0;
contract MyContractTest {
function test_Requirements() external {
// It should match the output of a high-level hash.
// It should never revert.
}
modifier whenSomePreconditionOccurs() {
_;
}
function test_WhenSomePreconditionOccurs()
external
whenSomePreconditionOccurs
{
// it should do an action.
// it should do another action.
}
} because the structure of the tests depends on the function being tested. The only change to the tree above that wouldn't change the structure of the tests is adding an action as a child of the condition, like so:
But you would say it'd be less surprising if the following change didn't change the structure either:
After laying it out, I'm less confident in either implementation haha, both make sense to me, so I'm happy to implement it the other way. Will wait for Paul to chime in before updating the implementation. One argument I can think of against the current one is that it'd be more work in
I thought testing function invariants + possible scenarios would be a totally normal use case, so I didn't want to restrict it. Let's take the function that calls function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) {
return a < b ? _efficientHash(a, b) : _efficientHash(b, a);
} A dummy (but reasonable) tree for this function might be:
wdyt? Sorry for the long post, I just want to get this right. |
No worries at all, this is a valuable discussion and I appreciate it!
This is a great example. It's made me realize that my spec (and consequently this spec) are poorly specified. Given that there are two arguments,
Given that, we can ask:
For (1), I think having all root-level Now let's say we add an arbitrary restriction that zero values are not allowed. This gives a new spec:
First off, I'll note this currently generates invalid solidity code— But now it makes less sense to have a |
Yeah, I think that is reasonable. My argument was that one would want to test the parts of the function that should always be true separately, but I am not confident in this + it would be more work later in
Argh, I really need to improve the tests. If it were not broken (I probably introduced a bug recently), the implementation idea was to generate EDIT: Fixed it (introduced a bug a few versions ago) and the output is: pragma solidity 0.8.0;
contract MyContractTest {
function test_ShouldNeverRevert() external {
// It should never revert.
}
modifier whenFirstArgIsSmallerThanSecondArg() {
_;
}
function test_WhenFirstArgIsSmallerThanSecondArg()
external
whenFirstArgIsSmallerThanSecondArg
{
// It should match the result of `keccak256(abi.encodePacked(a,b))`.
}
modifier whenFirstArgIsBiggerThanSecondArg() {
_;
}
function test_WhenFirstArgIsBiggerThanSecondArg()
external
whenFirstArgIsBiggerThanSecondArg
{
// It should match the result of `keccak256(abi.encodePacked(b,a))`.
}
}
I think this makes sense! When something happens, we might want to assert something and then just explore the rest of the function paths with other conditions. However, I don't think this occurs in the Sablier codebase. They usually have:
i.e. they nest conditions, which may inform us. Yet another ping @PaulRBerg 🙈 |
Hmm @PaulRBerg @alexfertel what do you think about the current repetitiveness? For example: function test_WhenFirstArgIsSmallerThanSecondArg()
external
whenFirstArgIsSmallerThanSecondArg
{
// It should match the result of `keccak256(abi.encodePacked(a,b))`.
} We have |
Yes @mds1 , this is being discussed here #7. I am trying to capture exactly what the Sablier team does, but can't quite figure it out. My latest understanding is:
However, as you can see in my answer, that is not entirely accurate. Btw, I'm currently working on |
Ah thanks, will check out that discussion! |
Discussed in #18
Originally posted by mds1 August 28, 2023
Bulloak looks really nice and I'm excited to try it out. I have a function like this OZ function:
There's no branching or conditionals here so I expected my
.tree
file to look very simple:But running
bulloak
against this errors with:I could add a "when called" condition, but this would add unnecessary clutter to the tree and tests since there's no conditions or paths in this method.
Should bulloak/BTT support specs of this style, or should I be doing things differently here? I think a good approach might be to only allow this IF all requirements (
it should
lines) are at the shallowest level and not preceded by any given's or when's.Related, I'd like to see a bit more info in the README on the requirements around
.tree
files, e.g. when arewhen
andgiven
required, can you only have one of the two, can their order be flipped or must it bewhen > given
, etc. The reason I ask here is I want to mirror the given > when > then flow of cucumber vs. the when > given > it should of BTT (perhaps both can be supported?)cc @PaulRBerg for thoughts too
Instructions:
Ast::Action
while visiting the root.bulloak/src/semantics.rs
Lines 157 to 160 in 3b4c0dc
bulloak/src/emitter.rs
Lines 271 to 280 in 3b4c0dc
The text was updated successfully, but these errors were encountered: