From bacbaca921ed59559f3e05307f0cab64562c5608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20T=C3=B3th?= Date: Tue, 5 May 2026 11:25:13 +0000 Subject: [PATCH 1/4] Lift option `--directory` to the supercommand level --- README.md | 10 ++++++---- src/skribe/__main__.py | 16 +++++----------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index af3dff4..1ff6acd 100644 --- a/README.md +++ b/README.md @@ -15,15 +15,17 @@ Skribe has two subcommands: `build` and `run`. ``` $ skribe --help -usage: skribe [-h] {build,run} ... +usage: skribe [-h] [--directory DIRECTORY] {build,run} ... positional arguments: {build,run} - build build the test contract - run run tests with fuzzing + build build the test contract + 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 diff --git a/src/skribe/__main__.py b/src/skribe/__main__.py index fb37c81..9ba4677 100644 --- a/src/skribe/__main__.py +++ b/src/skribe/__main__.py @@ -74,10 +74,7 @@ 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, @@ -85,14 +82,11 @@ def _argument_parser() -> ArgumentParser: 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') + 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.' ) From 170731d594c4acc1e6273426b58781f937b57135 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20T=C3=B3th?= Date: Tue, 5 May 2026 11:46:52 +0000 Subject: [PATCH 2/4] Add subcommand `export-specs` --- README.md | 5 +++-- src/skribe/__main__.py | 33 +++++++++++++++++++++++++++++---- src/skribe/skribe.py | 21 +++++++++++++-------- 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 1ff6acd..e3a5b7a 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,12 @@ Skribe has two subcommands: `build` and `run`. ``` $ skribe --help -usage: skribe [-h] [--directory DIRECTORY] {build,run} ... +usage: skribe [-h] [--directory DIRECTORY] {build,export-specs,run} ... positional arguments: - {build,run} + {build,export-specs,run} build build the test contract + export-specs print the fuzzer specifications run run tests with fuzzing options: diff --git a/src/skribe/__main__.py b/src/skribe/__main__.py index 9ba4677..7fcee6e 100644 --- a/src/skribe/__main__.py +++ b/src/skribe/__main__.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json import sys from argparse import ArgumentParser from pathlib import Path @@ -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.export_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. @@ -85,6 +106,7 @@ def _argument_parser() -> ArgumentParser: 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( @@ -103,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}') diff --git a/src/skribe/skribe.py b/src/skribe/skribe.py index 9f2b842..2f74f5f 100644 --- a/src/skribe/skribe.py +++ b/src/skribe/skribe.py @@ -206,14 +206,7 @@ def calldata_to_kore(data: bytes) -> Pattern: task.end() def deploy_and_run(self, max_examples: int, id: str | None = None) -> list[FuzzError]: - - 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_] + test_contracts = self._load_contracts() errors: list[FuzzError] = [] for contract in test_contracts: @@ -221,6 +214,14 @@ def deploy_and_run(self, max_examples: int, id: str | None = None) -> list[FuzzE return errors + 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 deploy_and_run_contract( self, contract: ArbitrumContract, max_examples: int, id: str | None = None ) -> list[FuzzError]: @@ -238,6 +239,10 @@ def deploy_and_run_contract( return errors + def export_specs(self) -> list[FuzzSpec]: + contracts = self._load_contracts() + return [self.create_spec(contract) for contract in contracts] + 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) From ff4461eace76d697c8d6c4a4d2d6b197e87123ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20T=C3=B3th?= Date: Tue, 5 May 2026 13:06:10 +0000 Subject: [PATCH 3/4] Merge code paths for `run` and `export-specs` --- src/skribe/__main__.py | 2 +- src/skribe/skribe.py | 40 ++++++++++++++++++++-------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/skribe/__main__.py b/src/skribe/__main__.py index 7fcee6e..5771e0c 100644 --- a/src/skribe/__main__.py +++ b/src/skribe/__main__.py @@ -49,7 +49,7 @@ def _exec_export_specs(dir_path: Path | None) -> None: """ dir_path = Path.cwd() if dir_path is None else dir_path skribe = Skribe(concrete_definition, dir_path) - specs = skribe.export_specs() + specs = skribe.init_specs() print(json.dumps([spec.dict for spec in specs])) exit(0) diff --git a/src/skribe/skribe.py b/src/skribe/skribe.py index 2f74f5f..2162eca 100644 --- a/src/skribe/skribe.py +++ b/src/skribe/skribe.py @@ -206,26 +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]: - test_contracts = self._load_contracts() + # Deploy + specs = self.init_specs() + # 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 _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 init_specs(self) -> list[FuzzSpec]: + contracts = self._load_contracts() + specs = [self._create_spec(contract) for contract in contracts] + return specs - def deploy_and_run_contract( - self, contract: ArbitrumContract, max_examples: int, id: str | None = None - ) -> list[FuzzError]: - spec = self.create_spec(contract) + 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] = [] @@ -239,16 +235,20 @@ def deploy_and_run_contract( return errors - def export_specs(self) -> list[FuzzSpec]: - contracts = self._load_contracts() - return [self.create_spec(contract) for contract in contracts] + 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) + 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)) From be684bb86d87b5518396e221d4596bd126509918 Mon Sep 17 00:00:00 2001 From: devops Date: Wed, 6 May 2026 12:33:07 +0000 Subject: [PATCH 4/4] Set Version: 0.1.31 --- package/version | 2 +- pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package/version b/package/version index 013adb7..db7a480 100644 --- a/package/version +++ b/package/version @@ -1 +1 @@ -0.1.30 +0.1.31 diff --git a/pyproject.toml b/pyproject.toml index 94d403c..9ba6a03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/uv.lock b/uv.lock index 3cb0ea9..5cc0e66 100644 --- a/uv.lock +++ b/uv.lock @@ -1815,7 +1815,7 @@ wheels = [ [[package]] name = "skribe" -version = "0.1.30" +version = "0.1.31" source = { editable = "." } dependencies = [ { name = "kontrol" },