A Cairo test generator based on the Branching Tree Technique and bulloak.
cargo install poinciana --git https://github.com/ericnordelo/poincianaThe following VSCode extensions are not essential but they are recommended for a better user experience:
- Ascii Tree Generator: convenient way to generate ASCII trees
poinciana implements two commands:
poinciana scaffoldpoinciana check(in the roadmap)
Say you have a foo.tree file with the following contents:
FooTest
└── When stuff is called // Comments are supported.
└── When a condition is met
└── It should revert.
└── Because we shouldn't allow it.
You can use poinciana scaffold to generate a Cairo contract containing
modifiers and tests that match the spec described in foo.tree. The following
will be printed to stdout:
/// Generated by poinciana using BTT
fn when_stuff_is_called() {
// code
}
#[test]
fn test_panic_when_a_condition_is_met() {
when_stuff_is_called();
// It should revert.
// Because we shouldn't allow it.
panic!("NOT IMPLEMENTED");
}ponciana scaffold scaffolds Cairo test files based on .tree specifications
that follow the
Branching Tree Technique. The tree parser implementation is currently coming from bulloak, which is Solidity optimized. In the future we may update the tree syntax to better fit the Cairo ecosystem.
Currently, there is on-going discussion on how to handle different edge-cases to better empower the Solidity community. This section is a description of the current implementation of the bulloak compiler.
- Condition:
when/givenbranches of a tree. - Action:
itbranches of a tree. - Action Description: Children of an action.
Each tree file should describe at least one function under test. Trees follow
these rules:
- The first line is the root tree identifier, composed of the module (contract or component) and function names which should be delimited by a double colon.
poincianaexpects you to use├and└characters to denote branches.- If a branch starts with either
whenorgiven, it is a condition.whenandgivenare interchangeable.
- If a branch starts with
it, it is an action.- Any child branch an action has is called an action description.
- Keywords are case-insensitive:
itis the same asItandIT. - Anything starting with a
//is a comment and will be stripped from the output. - Multiple trees can be defined in the same file to describe different functions by following the same rules, separating them with two newlines.
Take the following Cairo function:
fn hash_pair(a: u256, b: u256) -> u256 {
if (a < b) {
hash(a, b)
} else {
hash(b, a)
}
}A reasonable spec for the above function would be:
HashPairTest
├── It should never revert.
├── When first arg is smaller than second arg
│ └── It should match the result of `hash(a, b)`.
└── When first arg is bigger than second arg
└── It should match the result of `hash(b, a)`.
There is a top-level action that will generate a test to check the function invariant that it should never revert.
Then, we have the two possible preconditions: a < b and a >= b. Both
branches end in an action that will make poinciana scaffold generate the
respective test.
Note the following things:
- Actions are written with ending dots but conditions are not. This is because actions support any character, but conditions don't. Since conditions are transformed into modifiers, they have to be valid Cairo identifiers.
- You can have top-level actions without conditions. Currently,
poincianaalso supports actions with sibling conditions, but this might get removed in a future version per this discussion. - The root of the tree will be emitted as the name of the test contract.
Suppose you have additional Cairo functions that you want to test in the same
test contract, say Utils within utils.t.cairo:
fn min(a: u256, b: u256) -> u256 {
if a < b { a } else { b }
}
fn max(a: u256, b: u256) -> u256 {
if a > b { a } else { b }
}The full spec for all the above functions would be:
Utils::hash_pair
├── It should never revert.
├── When first arg is smaller than second arg
│ └── It should match the result of `hash(a, b)`.
└── When first arg is bigger than second arg
└── It should match the result of `hash(b, a)`.
Utils::min
├── It should never revert.
├── When first arg is smaller than second arg
│ └── It should match the value of `a`.
└── When first arg is bigger than second arg
└── It should match the value of `b`.
Utils::max
├── It should never revert.
├── When first arg is smaller than second arg
│ └── It should match the value of `b`.
└── When first arg is bigger than second arg
└── It should match the value of `a`.
Note the following things:
- Module identifiers must be present in all roots.
- Module identifiers that are missing from subsequent trees, or otherwise
mismatched from the first tree root identifier, will cause
poincianato error. - Duplicate conditions between separate trees will be deduplicated when transformed into Cairo modifiers (helpers).
- The function part of the root identifier for each tree will be emitted as part
of the name of the Cairo test (e.g.
test_min_should_never_revert).
This project is licensed under either of:
- Apache License, Version 2.0, (LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0).
- MIT license (LICENSE-MIT or https://opensource.org/licenses/MIT).