Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,18 @@ Skribe has two subcommands: `build` and `run`.

```
$ skribe --help
usage: skribe [-h] {build,run} ...
usage: skribe [-h] [--directory DIRECTORY] {build,export-specs,run} ...

positional arguments:
{build,run}
build build the test contract
run run tests with fuzzing
{build,export-specs,run}
build build the test contract
export-specs print the fuzzer specifications
run run tests with fuzzing

options:
-h, --help show this help message and exit
-h, --help show this help message and exit
--directory, -C DIRECTORY
The test contract's directory (defaults to the current working directory).
```
### Skribe test contract structure

Expand Down
2 changes: 1 addition & 1 deletion package/version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.30
0.1.31
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "skribe"
version = "0.1.30"
version = "0.1.31"
description = "Property testing for Stylus smart contracts"
readme = "README.md"
requires-python = "~=3.10"
Expand Down
49 changes: 34 additions & 15 deletions src/skribe/__main__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import json
import sys
from argparse import ArgumentParser
from pathlib import Path
Expand Down Expand Up @@ -33,6 +34,26 @@ def _exec_build(dir_path: Path | None) -> None:
exit(0)


def _exec_export_specs(dir_path: Path | None) -> None:
"""
Exports the fuzzer specifications for the contracts located in the specified directory.

If `dir_path` is None, the export is executed in the current working directory (CWD).

Args:
dir_path (Path | None): Path to the directory containing the contract sources.
If None, defaults to the current working directory.

Returns:
None
"""
dir_path = Path.cwd() if dir_path is None else dir_path
skribe = Skribe(concrete_definition, dir_path)
specs = skribe.init_specs()
print(json.dumps([spec.dict for spec in specs]))
exit(0)


def _exec_run(dir_path: Path | None, id: str | None, max_examples: int) -> None:
"""
Executes fuzz tests for the Skribe test contract located at the given path.
Expand Down Expand Up @@ -74,25 +95,20 @@ def _exec_run(dir_path: Path | None, id: str | None, max_examples: int) -> None:

def _argument_parser() -> ArgumentParser:
parser = ArgumentParser(prog='skribe')
command_parser = parser.add_subparsers(dest='command', required=True)

build_parser = command_parser.add_parser('build', help='build the test contract')
build_parser.add_argument(
parser.add_argument(
'--directory',
'-C',
type=ensure_dir_path,
default=None,
help="The test contract\'s directory (defaults to the current working directory).",
)

command_parser = parser.add_subparsers(dest='command', required=True)

command_parser.add_parser('build', help='build the test contract')
command_parser.add_parser('export-specs', help='print the fuzzer specifications')

run_parser = command_parser.add_parser('run', help='run tests with fuzzing')
run_parser.add_argument(
'--directory',
'-C',
type=ensure_dir_path,
default=None,
help="The test contract\'s directory (defaults to the current working directory).",
)
run_parser.add_argument(
'--id', help='Name of the test function to run. If not specified, all test functions will be executed.'
)
Expand All @@ -109,9 +125,12 @@ def main() -> None:
parser = _argument_parser()
args, rest = parser.parse_known_args()

if args.command == 'run':
_exec_run(dir_path=args.directory, id=args.id, max_examples=args.max_examples)
elif args.command == 'build':
_exec_build(dir_path=args.directory)
match args.command:
case 'run':
_exec_run(dir_path=args.directory, id=args.id, max_examples=args.max_examples)
case 'build':
_exec_build(dir_path=args.directory)
case 'export-specs':
_exec_export_specs(dir_path=args.directory)

raise RuntimeError(f'Command not implemented: {args.command}')
39 changes: 22 additions & 17 deletions src/skribe/skribe.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,25 +206,22 @@ def calldata_to_kore(data: bytes) -> Pattern:
task.end()

def deploy_and_run(self, max_examples: int, id: str | None = None) -> list[FuzzError]:
# Deploy
specs = self.init_specs()

test_contracts: list[ArbitrumContract]
if self.is_foundry:
foundry = Foundry(self.contract_dir)
test_contracts = [c for c in foundry.contracts.values() if is_foundry_test(c)]
else:
contract_ = StylusContract(cargo_bin=self._cargo_bin, contract_dir=self.contract_dir)
test_contracts = [contract_]

# Run
errors: list[FuzzError] = []
for contract in test_contracts:
errors += self.deploy_and_run_contract(contract, max_examples, id)
for spec in specs:
errors += self._run_spec(spec, max_examples, id)

return errors

def deploy_and_run_contract(
self, contract: ArbitrumContract, max_examples: int, id: str | None = None
) -> list[FuzzError]:
spec = self.create_spec(contract)
def init_specs(self) -> list[FuzzSpec]:
contracts = self._load_contracts()
specs = [self._create_spec(contract) for contract in contracts]
return specs

def _run_spec(self, spec: FuzzSpec, max_examples: int, id: str | None = None) -> list[FuzzError]:
signatures = _filter_signatures(spec.signatures, id=id)

errors: list[FuzzError] = []
Expand All @@ -238,12 +235,20 @@ def deploy_and_run_contract(

return errors

def create_spec(self, contract: ArbitrumContract) -> FuzzSpec:
template = self.create_template_pattern(contract)
def _load_contracts(self) -> list[ArbitrumContract]:
if self.is_foundry:
foundry = Foundry(self.contract_dir)
return [c for c in foundry.contracts.values() if is_foundry_test(c)]

contract = StylusContract(cargo_bin=self._cargo_bin, contract_dir=self.contract_dir)
return [contract]

def _create_spec(self, contract: ArbitrumContract) -> FuzzSpec:
template = self._create_template_pattern(contract)
signatures = tuple(Signature.from_method(method) for method in contract.methods if method.is_test)
return FuzzSpec(template=template, signatures=signatures)

def create_template_pattern(self, contract: ArbitrumContract) -> Pattern:
def _create_template_pattern(self, contract: ArbitrumContract) -> Pattern:
contract_kast: KInner
if isinstance(contract, StylusContract):
contract_kast = wasm2kast(BytesIO(contract.deployed_bytecode))
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading