poinciana

A Cairo test generator based on the Branching Tree Technique.

APACHE-2.0 License

Stars
2

poinciana

A Cairo test generator based on the Branching Tree Technique and bulloak.

Installation

cargo install poinciana --git https://github.com/ericnordelo/poinciana

VSCode

The following VSCode extensions are not essential but they are recommended for a better user experience:

Usage

poinciana implements two commands:

  • poinciana scaffold
  • poinciana check (in the roadmap)

Scaffold Cairo Files

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");
}

Trees

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.

Terminology

  • Condition: when/given branches of a tree.
  • Action: it branches of a tree.
  • Action Description: Children of an action.

Spec

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.
  • poinciana expects you to use and characters to denote branches.
  • If a branch starts with either when or given, it is a condition.
    • when and given are 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: it is the same as It and IT.
  • 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, poinciana also
    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 poinciana to 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).

License

This project is licensed under either of: