Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for schedulers #372

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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 Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ include = [
arbitrary = { version = "1.4.1", features = ["derive"], optional = true }
async-trait = "0.1.83"
bitflags = "2.6.0"
rayon = { version = "1.10.0", optional = true }
tokio = { version = "1.41.1", optional = true, features = ["rt"] }

[dev-dependencies]
tokio = { version = "1.41.1", features = [
Expand All @@ -33,3 +35,5 @@ tokio = { version = "1.41.1", features = [

[features]
arbitrary = ["dep:arbitrary", "bitflags/arbitrary"]
async-tokio-scheduler = ["dep:tokio"]
threaded-scheduler = ["dep:rayon"]
74 changes: 64 additions & 10 deletions bindings/python/Cargo.lock

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

12 changes: 8 additions & 4 deletions bindings/python/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@ name = "openchecks"
crate-type = ["cdylib"]

[dependencies]
pyo3 = { version = "0.23.5" }
# base_openchecks = { git = "https://github.com/scott-wilson/openchecks.git", rev = "b57d12aec35feebdf5220ae8544a2782539ce6da", package = "openchecks" }
base_openchecks = { path = "../../", package = "openchecks" }

async-trait = "0.1.83"
pyo3 = { version = "0.23.3" }
base_openchecks = { path = "../../", package = "openchecks", features = [
"async-tokio-scheduler",
"threaded-scheduler",
] }
pyo3-async-runtimes = { version = "0.23.0", features = [
"attributes",
"tokio-runtime",
] }
async-trait = "0.1.88"
rayon = "1.10.0"
200 changes: 200 additions & 0 deletions bindings/python/fuzz/scheduler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
# ruff: noqa: D103,D100,D101,D102,D107,S101

from __future__ import annotations

from typing import TYPE_CHECKING

import atheris
import hypothesis
import openchecks
from hypothesis import strategies

if TYPE_CHECKING: # pragma: no cover
from typing import List, Optional


class Check(openchecks.BaseCheck):
def __init__(
self,
title: str,
description: str,
hint: openchecks.CheckHint,
status: openchecks.Status,
fix_status: openchecks.Status,
message: str,
items: Optional[List[openchecks.Item[int]]],
can_fix: bool,
can_skip: bool,
error: Optional[BaseException],
) -> None:
self._title = title
self._description = description
self._hint = hint
self._status = status
self._fix_status = fix_status
self._message = message
self._items = items
self._can_fix = can_fix
self._can_skip = can_skip
self._error = error

def title(self) -> str:
return self._title

def description(self) -> str:
return self._description

def hint(self) -> openchecks.CheckHint:
return self._hint

def check(self) -> openchecks.CheckResult[int]:
return openchecks.CheckResult(
self._status,
self._message,
self._items,
self._can_fix,
self._can_skip,
self._error,
)

def auto_fix(self) -> None:
if self._error:
raise self._error

self._status = self._fix_status


@strategies.composite
def check_hints(draw: strategies.DrawFn) -> openchecks.CheckHint:
hint = openchecks.CheckHint.NONE

if draw(strategies.booleans()):
hint |= openchecks.CheckHint.AUTO_FIX

return hint


@hypothesis.given(
checks=strategies.builds(
Check,
title=strategies.text(),
description=strategies.text(),
hint=check_hints(),
status=strategies.sampled_from(
[
openchecks.Status.Pending,
openchecks.Status.Skipped,
openchecks.Status.Passed,
openchecks.Status.Warning,
openchecks.Status.Failed,
openchecks.Status.SystemError,
]
),
fix_status=strategies.sampled_from(
[
openchecks.Status.Pending,
openchecks.Status.Skipped,
openchecks.Status.Passed,
openchecks.Status.Warning,
openchecks.Status.Failed,
openchecks.Status.SystemError,
]
),
message=strategies.text(),
items=strategies.one_of(
strategies.none(),
strategies.lists(
strategies.builds(
openchecks.Item,
value=strategies.integers(),
type_hint=strategies.one_of(strategies.none(), strategies.text()),
)
),
),
can_fix=strategies.booleans(),
can_skip=strategies.booleans(),
error=strategies.one_of(
strategies.none(), strategies.builds(Exception, strategies.text())
),
)
)
@atheris.instrument_func
def fuzz(
checks: List[Check],
) -> None:
scheduler = openchecks.Scheduler()
results = scheduler.run(checks)
auto_fix_checks = []

for check, result in results:
assert isinstance(check, Check)
assert check.title() == check._title
assert check.description() == check._description
assert check.hint() == check._hint

result = check.check()

assert result.status() == check._status
assert result.message() == check._message
assert result.items() == check._items

if check._error:
assert isinstance(result.error(), openchecks.CheckError)

assert str(result.error()) == str(check._error)
else:
assert result.error() is None

if result.status() == openchecks.Status.SystemError:
assert result.can_fix() is False
assert result.can_skip() is False
else:
assert result.can_fix() == check._can_fix
assert result.can_skip() == check._can_skip

if result.status().has_failed() and result.can_fix():
auto_fix_checks.append(check)

fix_results = scheduler.auto_fix(auto_fix_checks)

for check, fix_result in fix_results:
assert isinstance(check, Check)

if not check.hint() & openchecks.CheckHint.AUTO_FIX:
assert fix_result.status() == openchecks.Status.SystemError
assert fix_result.message() == "Check does not implement auto fix."
assert fix_result.items() is None
assert fix_result.error() is None
elif fix_result.error():
assert fix_result.status() == openchecks.Status.SystemError
assert fix_result.message() == "Error in auto fix."
assert fix_result.items() is None

if check._error:
assert isinstance(fix_result.error(), openchecks.CheckError)

assert (
str(fix_result.error())
== f"{check._error.__class__.__name__}: {check._error}"
)
else:
assert result.error() is None
else:
assert fix_result.status() == check._fix_status
assert fix_result.message() == check._message
assert fix_result.items() == check._items
assert fix_result.error() is None

if fix_result.status() == openchecks.Status.SystemError:
assert fix_result.can_fix() is False
assert fix_result.can_skip() is False
else:
assert fix_result.can_fix() == check._can_fix
assert fix_result.can_skip() == check._can_skip


if __name__ == "__main__":
import sys

atheris.Setup(sys.argv, atheris.instrument_func(fuzz.hypothesis.fuzz_one_input))
atheris.Fuzz()
Loading
Loading