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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ src/hiero_sdk_python/hapi
# Pytest
.pytest_cache

# Hypothesis state
.hypothesis/

# Lock files
uv.lock
pdm.lock
pubkey.asc
pubkey.asc
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
- Added TCK endpoint for the createAccount method
- Renamed `delegate_contract_id.py` to `delegate_contract_id_test.py` (#2004)
- Fix Flaky tests for `mock_server` by enforcing non-tls port and adding a mock_tls certificate
- Implement basic fuzz testing [#1872](https://github.com/hiero-ledger/hiero-sdk-python/issues/1872)


### Docs
- Add Chocolatey as a prerequisite in the Windows setup guide (#1961)
Expand Down Expand Up @@ -118,8 +120,6 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
- docs: Clarify issues need to be assigned in template files. (#1884)
- doc: Fix testnet link in README.md. (#1879)



### Tests
- Format `tests/unit/endpoint_test.py` using black. (`#1792`)
- Implement TCK JSON-RPC server with request handling and error management
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ dev = [
"grpcio-tools>=1.76.0,<2",
"pytest>=8.3.4,<10",
"pytest-cov>=7.0.0,<8",
"hypothesis>=6.137.2"
]

lint = [
Expand Down
3 changes: 2 additions & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ pythonpath = src

markers =
integration: mark a test as an integration test.
unit: mark a test as a unit test.
unit: mark a test as a unit test.
fuzz: mark a test as fuzz test.
1 change: 1 addition & 0 deletions tests/fuzz/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# with <3 from Anto
Comment thread
AntonioCeppellini marked this conversation as resolved.
32 changes: 32 additions & 0 deletions tests/fuzz/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Shared Hypothesis setup, fixtures, and compatibility re-exports for fuzz tests."""

from tests.fuzz.support.classes import (
AccountIdAliasCase,
ContractValueCase,
EntityIdCase,
HbarConstructorCase,
HbarStringCase,
InvalidContractValueCase,
)
from tests.fuzz.support.profiles import load_hypothesis_profile
from tests.fuzz.support.registry import (
FUZZ_STRATEGIES,
fuzz_strategies_fixture,
get_strategy,
get_strategy_fixture,
)

load_hypothesis_profile()

__all__ = [
"AccountIdAliasCase",
"ContractValueCase",
"EntityIdCase",
"FUZZ_STRATEGIES",
"HbarConstructorCase",
"HbarStringCase",
"InvalidContractValueCase",
"fuzz_strategies_fixture",
"get_strategy",
"get_strategy_fixture",
]
20 changes: 20 additions & 0 deletions tests/fuzz/support/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from tests.fuzz.support.classes import (
AccountIdAliasCase,
ContractValueCase,
EntityIdCase,
HbarConstructorCase,
HbarStringCase,
InvalidContractValueCase,
)
from tests.fuzz.support.registry import FUZZ_STRATEGIES, get_strategy

__all__ = [
"AccountIdAliasCase",
"ContractValueCase",
"EntityIdCase",
"FUZZ_STRATEGIES",
"HbarConstructorCase",
"HbarStringCase",
"InvalidContractValueCase",
"get_strategy",
]
61 changes: 61 additions & 0 deletions tests/fuzz/support/classes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from dataclasses import dataclass
from decimal import Decimal
from typing import Any

from hiero_sdk_python import HbarUnit


@dataclass(frozen=True)
class EntityIdCase:
"""A parsed entity ID expectation for public string parsers."""

text: str
shard: int
realm: int
value: int
checksum: str | None = None


@dataclass(frozen=True)
class AccountIdAliasCase:
"""A valid account alias or EVM-address input."""

text: str
shard: int
realm: int
alias_hex: str | None = None
evm_hex: str | None = None


@dataclass(frozen=True)
class HbarStringCase:
"""A valid public Hbar string and its exact tinybar value."""

text: str
tinybars: int


@dataclass(frozen=True)
class HbarConstructorCase:
"""A valid Hbar constructor input and its exact tinybar value."""

amount: int | float | Decimal
unit: HbarUnit
tinybars: int


@dataclass(frozen=True)
class ContractValueCase:
"""A valid contract parameter case routed to an explicit public add_* method."""

method_name: str
value: Any


@dataclass(frozen=True)
class InvalidContractValueCase:
"""An invalid contract parameter case with a precise expected exception."""

method_name: str
value: Any
expected_exception: type[BaseException]
65 changes: 65 additions & 0 deletions tests/fuzz/support/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from decimal import Decimal

from hypothesis import strategies as st
from hypothesis.strategies import SearchStrategy

from hiero_sdk_python import AccountId, HbarUnit, PrivateKey, TransactionId, TransferTransaction

from tests.fuzz.support.classes import HbarConstructorCase, HbarStringCase


def sized_hex(byte_length: int) -> SearchStrategy[str]:
"""Return a fixed-length hex string strategy."""
return st.binary(min_size=byte_length, max_size=byte_length).map(bytes.hex)


def with_optional_0x(hex_strategy: SearchStrategy[str]) -> SearchStrategy[str]:
"""Allow a hex string with or without the `0x` prefix."""
return st.one_of(hex_strategy, hex_strategy.map(lambda value: f"0x{value}"))


def decimal_string(value: Decimal) -> str:
"""Format a Decimal without trailing zeros."""
text = format(value, "f")
if "." in text:
text = text.rstrip("0").rstrip(".")
return text or "0"


def hbar_string_case(unit: HbarUnit, tinybars: int) -> HbarStringCase:
"""Build a valid Hbar string case from an exact tinybar amount."""
amount = Decimal(tinybars) / Decimal(unit.tinybar)
if unit == HbarUnit.HBAR:
return HbarStringCase(text=decimal_string(amount), tinybars=tinybars)
return HbarStringCase(text=f"{decimal_string(amount)} {unit.symbol}", tinybars=tinybars)


def hbar_constructor_case(unit: HbarUnit, tinybars: int) -> HbarConstructorCase:
"""Build a valid Hbar constructor case from an exact tinybar amount."""
if unit == HbarUnit.TINYBAR:
amount: int | float | Decimal = tinybars
else:
amount = Decimal(tinybars) / Decimal(unit.tinybar)
return HbarConstructorCase(amount=amount, unit=unit, tinybars=tinybars)


def build_valid_transaction_bytes() -> tuple[bytes, bytes]:
"""Build one valid unsigned and one valid signed transaction payload."""
operator_id = AccountId.from_string("0.0.1234")
node_id = AccountId.from_string("0.0.3")
receiver_id = AccountId.from_string("0.0.5678")

tx = (
TransferTransaction()
.add_hbar_transfer(operator_id, -100_000_000)
.add_hbar_transfer(receiver_id, 100_000_000)
)
tx.transaction_id = TransactionId.generate(operator_id)
tx.node_account_id = node_id
tx.freeze()
unsigned_bytes = tx.to_bytes()

signed_tx = TransferTransaction.from_bytes(unsigned_bytes)
signed_tx.sign(PrivateKey.from_string_ed25519("02" * 32))
signed_bytes = signed_tx.to_bytes()
return unsigned_bytes, signed_bytes
31 changes: 31 additions & 0 deletions tests/fuzz/support/profiles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import os

from hypothesis import HealthCheck, settings


def load_hypothesis_profile() -> None:
"""Register and load the active Hypothesis profile."""
settings.register_profile(
"ci",
settings(
derandomize=True,
max_examples=300,
deadline=750,
suppress_health_check=[HealthCheck.too_slow],
),
)
settings.register_profile(
"local",
settings(
derandomize=False,
max_examples=1000,
deadline=None,
),
)

requested = os.getenv("HYPOTHESIS_PROFILE")
if requested:
settings.load_profile(requested)
return

settings.load_profile("ci" if os.getenv("CI") else "local")
Loading
Loading