Prerequsites: K framework, python >= 3.10
, pip >= 20.0.2
, uv
.
make build
pip install dist/*.whl
Skribe has two subcommands: build
and run
.
$ skribe --help
usage: skribe [-h] {build,run} ...
positional arguments:
{build,run}
build build the test contract
run run tests with fuzzing
options:
-h, --help show this help message and exit
A Skribe test contract is a regular Stylus contract written in Rust, organized in a way that allows Skribe to discover and execute test functions automatically. It can include an optional init function for setup and any number of test functions following a specific naming and return convention.
The test contract may optionally define an init
function, which Skribe calls once before executing any tests. This
function serves as a workaround for Stylus's current lack of constructor support and can be used to perform setup logic,
such as initializing state or linking to other contracts. If the init
function takes arguments, they must be contract
addresses referring to other contracts that the test will interact with. These are specified in a skribe.json
file,
which contains a "contracts"
field listing the paths to the relevant Wasm files. Skribe deploys these contracts ahead
of time and passes their addresses to the init
function in the given order.
Example contract with init
function:
#[public]
impl TestCounter {
pub fn init(&mut self, counter: Address) {
self.counter.set(counter);
}
// ...
}
Example skribe.json
file for the contract above:
{
"contracts": [
"../stylus-hello-world/target/wasm32-unknown-unknown/release/stylus_hello_world.wasm"
]
}
Test functions must start with the test_
prefix and return either bool
or ()
. A panic or a false
result is
considered a test failure. Skribe automatically discovers these test functions and runs them with randomized input
values as part of the fuzzing process.
Example test function:
#[public]
impl TestCounter {
// ...
pub fn test_call_set_get_number(&mut self, x: U256) -> bool {
let counter = ICounter::new(self.counter.get());
counter.set_number(Call::new_in(self), x).unwrap();
counter.number(self).unwrap() == x
}
}
Compile the test contract located in the specified directory.
skribe build --directory path/to/contract
If no directory is provided, Skribe defaults to the current working directory.
Options:
--directory
,-C
: Path to the test contract directory (default:.
)
Run fuzz tests on the test contract.
skribe run --directory path/to/contract --id test_function --max-examples 200
Options:
--directory
,-C
: Path to the test contract directory (default:.
)--id
: Name of a single test function to run. If not specified, Skribe runs all test functions.--max-examples
: Maximum number of fuzzing inputs to generate (default:100
)
The skribe run
command performs the following sequence of actions:
-
Create contracts Skribe reads the
skribe.json
file in the specified directory, and creates contracts from the provided Wasm files, obtaining their addresses. -
Initialize the test contract Skribe creates the test contract. If the test contract defines an
init
function, Skribe invokes it once before executing any tests, passing the addresses of the deployed child contracts in the order specified inskribe.json
. This allows for setup tasks such as linking to child contracts or initializing the blockchain state. -
Discover test functions Skribe scans the test contract for functions with names starting with the
test_
prefix, and displays them as a progress bar. -
Execute fuzz tests Skribe fuzzes the test functions, either all discovered or the one specified by the
--id
option—up to the limit set by--max-examples
or until a failure occurs. The progress of fuzzing each test function is displayed with a progress bar. -
Report results Failures are detected when a test function panics or returns
false
. Skribe reports any failing inputs and outcomes to the user.
cd src/tests/integration/data/contracts/test-hello-world
skribe build
skribe run --max-examples 500
skribe-simulation
is a CLI tool for simulating Stylus smart contracts.
Test cases are written in JSON as sequences of steps that:
- Deploy contracts
- Call functions
- Check expected results
Here is an example test case:
{
"steps": [
{
"type": "setExitCode", "value": 1
},
{
"type": "setStylusContract",
"id": 1,
"code": "path/to/contract.wasm"
},
{
"type": "callStylus",
"to": 1,
"data": {
"function": "number", "types": [], "args": []
},
"output": {
"type": "uint256",
"value": 1
},
"value": 0
},
{
"type": "setExitCode", "value": 0
}
]
}
This scenario:
- Sets the status code to 1 (indicating failure if the simulation doesn't finish),
- Deploys a Stylus contract from a WASM file,
- Calls the
number()
function on the contract and checks the output is 1, - Resets the expected exit code to 0 (indicating success).
Under the hood, JSON scenarios are translated to K terms and executed using Stylus formal semantics via krun
.
For debugging purposes, you can use skribe-simulation to generate an initial configuration term in Kore format and execute it with krun:
skribe-simulation run path/to/test.json --depth 0 > initial-state.kore
krun --definition $(kdist which stylus-semantics.llvm) initial-state.kore --parser cat --term
Use make
to run common tasks (see the Makefile for a complete list of available targets).
make build
: Build wheelmake check
: Check code stylemake format
: Format codemake test-unit
: Run unit testsmake test-integration
: Run integration tests