SkillAgentSearch skills...

Bulloak

Generate tests based on the Branching Tree Technique.

Install / Use

/learn @alexfertel/Bulloak
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

<p align="center"> <img src="https://github.com/user-attachments/assets/036bad22-3b0d-4ea3-9338-faecd017a290" width="200"></a> <br> <a href="https://crates.io/crates/bulloak/"> <img src="https://img.shields.io/crates/v/bulloak?style=flat&labelColor=1C2C2E&color=C96329&logo=Rust&logoColor=white"> </a> <a href="https://codecov.io/gh/alexfertel/bulloak"> <img src="https://codecov.io/github/alexfertel/bulloak/coverage.svg?branch=main"> </a> </p>

bulloak

A Solidity test generator based on the Branching Tree Technique.

<!-- prettier-ignore -->

[!WARNING] Note that bulloak is still 0.*.*, so breaking changes may occur at any time. If you must depend on bulloak, we recommend pinning to a specific version, i.e., =0.y.z.

Installation

cargo install bulloak

VSCode

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

Usage

bulloak implements two commands:

  • bulloak scaffold
  • bulloak check

Scaffold Solidity 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 bulloak scaffold to generate a Solidity contract containing modifiers and tests that match the spec described in foo.tree. The following will be printed to stdout:

// $ bulloak scaffold foo.tree
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.0;

contract FooTest {
    modifier whenStuffIsCalled() {
        _;
    }

    function test_RevertWhen_AConditionIsMet() external whenStuffIsCalled {
        // It should revert.
        //     Because we shouldn't allow it.
    }
}

You can use the -w option to write the generated contracts to the file system. Say we have a bunch of .tree files in the current working directory. If we run the following:

$ bulloak scaffold -w ./**/*.tree

bulloak will create a .t.sol file per .tree file and write the generated contents to it.

If a .t.sol file's title matches a .tree in the same directory, then bulloak will skip writing to that file. However, you may override this behavior with the -f flag. This will force bulloak to overwrite the contents of the file.

$ bulloak scaffold -wf ./**/*.tree

Note all tests are showing as passing when their body is empty. To prevent this, you can use the -S (or --vm-skip) option to add a vm.skip(true); at the beginning of each test function. This option will also add an import for forge-std's Test.sol and all test contracts will inherit from it.

You can skip emitting the modifier definitions by passing the -m (or --skip-modifiers) flag. Functions will still reference these modifiers in their signatures; only the modifier definitions themselves are omitted. This is useful together with bulloak check -m (which suppresses missing‑modifier violations). If you use -m alone, the scaffolded file will not compile until you provide the modifier definitions (or re-run without -m).

To normalize the generated comments, pass -F (or --format-descriptions). When enabled, bulloak capitalizes the first letter of each branch description and ensures it ends with a dot, so you don't need to touch the .tree file to get consistent sentence casing in the scaffolded test bodies.

Check That Your Code And Spec Match

You can use bulloak check to make sure that your Solidity files match your spec. For example, any missing tests will be reported to you.

Say you have the following spec:

HashPairTest
├── It should never revert.
├── When first arg is smaller than second arg
│   └── It should match the result of `keccak256(abi.encodePacked(a,b))`.
└── When first arg is bigger than second arg
    └── It should match the result of `keccak256(abi.encodePacked(b,a))`.

And a matching Solidity file:

pragma solidity 0.8.0;

contract HashPairTest {
  function test_ShouldNeverRevert() external {
    // It should never revert.
  }

  function test_WhenFirstArgIsSmallerThanSecondArg() external {
    // It should match the result of `keccak256(abi.encodePacked(a,b))`.
  }
}

This Solidity file is missing the tests for the branch When first arg is bigger than second arg, which would be reported after running bulloak check tests/scaffold/basic.tree, like so:

warn: function "test_WhenFirstArgIsBiggerThanSecondArg" is missing in .sol
     + fix: run `bulloak check --fix tests/scaffold/basic.tree`
   --> tests/scaffold/basic.tree:5

warn: 1 check failed (run `bulloak check --fix <.tree files>` to apply 1 fix)

As you can see in the above message, bulloak can fix the issue automatically. If we run the command with the --stdout flag, the output is:

--> tests/scaffold/basic.t.sol
pragma solidity 0.8.0;

contract HashPairTest {
    function test_ShouldNeverRevert() external {
        // It should never revert.
    }

    function test_WhenFirstArgIsSmallerThanSecondArg() external {
        // It should match the result of `keccak256(abi.encodePacked(a,b))`.
    }

    function test_WhenFirstArgIsBiggerThanSecondArg() external {
        // It should match the result of `keccak256(abi.encodePacked(b,a))`.
    }
}
<--

success: 1 issue fixed.

Running the command without the --stdout flag will overwrite the contents of the solidity file with the fixes applied. Note that not all issues can be automatically fixed, and bulloak's output will reflect that.

warn: 13 checks failed (run `bulloak check --fix <.tree files>` to apply 11 fixes)

You can skip checking that the modifiers are present by passing the -m (or --skip-modifiers) option. This way, bulloak will not warn when a modifier is missing from the generated file.

Use the same --format-descriptions flag when running bulloak check if you rely on the normalized comments. This keeps the structural matcher aligned with what bulloak scaffold --format-descriptions produces.

Rules

The following rules are currently implemented:

  • A Solidity file matching the spec file must exist and be readable.
    • The spec and the Solidity file match if the difference between their names is only .tree and .t.sol.
  • There is a contract in the Solidity file and its name matches the root node of the spec.
  • Every construct, as it would be generated by bulloak scaffold, is present in the Solidity file.
  • The order of every construct, as it would be generated by bulloak scaffold, matches the spec order.
    • Any valid Solidity construct is allowed and only constructs that would be generated by bulloak scaffold are checked. This means that any number of extra functions, modifiers, etc. can be added to the file.
  • Condition titles may repeat anywhere in a tree. bulloak reuses a single modifier definition per unique condition title and applies it wherever referenced.
  • Top‑level actions (leaves directly under the root) must have unique titles. bulloak cannot disambiguate these deterministically, so duplicates are reported as semantic errors.

Compiler Errors

Another feature of bulloak is reporting errors in your input trees.

For example, say you have a buggy foo.tree file, which is missing a character. Running bulloak scaffold foo.tree would report the error like this:

•••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••
bulloak error: unexpected `when` keyword

── when the id references a null stream
   ^^^^

--- (line 2, column 4) ---
file: foo.tree

Trees

bulloak scaffold scaffolds Solidity test files based on .tree specifications that follow the Branching Tree Technique.

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 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:

  • Single tree per file: the root can be just the contract name (e.g., FooTest).
  • Multiple trees in the same file: each root must be Contract::function, using :: as a separator, and all roots must share the same contract name (e.g., Foo::hashPair, Foo::min).
  • bulloak 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 comme

Related Skills

View on GitHub
GitHub Stars348
CategoryDevelopment
Updated2d ago
Forks23

Languages

Rust

Security Score

100/100

Audited on Apr 1, 2026

No findings