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
4 changes: 4 additions & 0 deletions docs-src/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
HypoFuzz uses [calendar-based versioning](https://calver.org/), with a
`YY-MM-patch` format.

## 25.02.2

Initial support for [stateful tests](https://hypothesis.readthedocs.io/en/latest/stateful.html).

## 25.02.1

Use a new mutator based on the typed choice sequence (https://github.com/HypothesisWorks/hypothesis/issues/3921), bringing back compatibility with new Hypothesis versions.
Expand Down
2 changes: 1 addition & 1 deletion src/hypofuzz/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Adaptive fuzzing for property-based tests using Hypothesis."""

__version__ = "25.02.1"
__version__ = "25.02.2"
__all__: list = []
33 changes: 26 additions & 7 deletions src/hypofuzz/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,22 @@
import sys
from collections.abc import Iterable
from contextlib import redirect_stdout
from functools import partial
from inspect import signature
from typing import TYPE_CHECKING, get_type_hints

import pytest
from _pytest.nodes import Item, Node
from _pytest.skipping import evaluate_condition
from hypothesis.stateful import RuleBasedStateMachine, run_state_machine_as_test
from hypothesis.stateful import get_state_machine_test
from packaging import version

if TYPE_CHECKING:
# We have to defer imports to within functions here, because this module
# is a Hypothesis entry point and is thus imported earlier than the others.
from .hy import FuzzProcess

pytest8 = version.parse(pytest.__version__) >= version.parse("8.0.0")


def has_true_skipif(item: Item) -> bool:
# multiple @skipif decorators are treated as an OR.
Expand Down Expand Up @@ -77,6 +79,7 @@ def pytest_collection_finish(self, session: pytest.Session) -> None:
# values directly, so we can pass them as extra kwargs to FuzzProcess.
params = item.callspec.params if hasattr(item, "callspec") else {}
param_names = set(params)
extra_kw = params

# Skip any test which:
# - directly requests a non autouse fixture, or
Expand All @@ -101,16 +104,32 @@ def pytest_collection_finish(self, session: pytest.Session) -> None:
flush=True,
)
continue

# Wrap it up in a FuzzTarget and we're done!
try:
# Skip state-machine classes, since they're not
if isinstance(item.obj, RuleBasedStateMachine.TestCase):
target = partial(run_state_machine_as_test, item.obj)
if hasattr(item.obj, "_hypothesis_state_machine_class"):
Comment thread
Liam-DeVoe marked this conversation as resolved.
assert (
extra_kw == {}
), "Not possible for RuleBasedStateMachine.TestCase to be parametrized"
runTest = item.obj
StateMachineClass = runTest._hypothesis_state_machine_class
target = get_state_machine_test( # type: ignore
StateMachineClass,
# runTest is a function, not a bound method, under pyest7.
# I wonder if something about TestCase instantiation order
# changed in pytest 8? Either way, we can't access
# __self__.settings under pytest 7.
#
# I am going to call this an acceptably rare bug for now,
# because it should only manifest if the user sets a custom
# database on a stateful test under pytest 7 (all non-db
# settings are ignored by hypofuzz).
settings=runTest.__self__.settings if pytest8 else None,
)
extra_kw = {"factory": StateMachineClass}
else:
target = item.obj
fuzz = FuzzProcess.from_hypothesis_test(
target, nodeid=item.nodeid, extra_kw=params
target, nodeid=item.nodeid, extra_kw=extra_kw
)
self.fuzz_targets.append(fuzz)
except Exception as err:
Expand Down
26 changes: 26 additions & 0 deletions tests/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def collect(code: str) -> list[FuzzProcess]:
"""
import pytest
from hypothesis import given, strategies as st
from hypothesis.stateful import RuleBasedStateMachine, Bundle, initialize, rule
"""
)
+ "\n"
Expand Down Expand Up @@ -152,3 +153,28 @@ def test_a(n):
pass
"""
assert not collect_names(code)


def test_collects_stateful_test():
code = """
names = st.text(min_size=1).filter(lambda x: "/" not in x)

class NumberModifier(RuleBasedStateMachine):
folders = Bundle("folders")
files = Bundle("files")

@initialize(target=folders)
def init_folders(self):
return "/"

@rule(target=folders, parent=folders, name=names)
def create_folder(self, parent, name):
return f"{parent}/{name}"

@rule(target=files, parent=folders, name=names)
def create_file(self, parent, name):
return f"{parent}/{name}"

NumberModifierTest = NumberModifier.TestCase
"""
assert collect_names(code) == {"run_state_machine"}