Skip to content

Commit

Permalink
test: adds tests for result
Browse files Browse the repository at this point in the history
  • Loading branch information
nf1s committed Feb 1, 2024
1 parent 26929ce commit 1e28a08
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 33 deletions.
18 changes: 16 additions & 2 deletions src/rules_engine/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
from typing import Any, Callable, TypeVar, Optional
from dataclasses import dataclass

T = TypeVar('T')


@dataclass
class Result:
value: Any
message: Optional[str]


class Rule:
def __init__(
self,
Expand All @@ -29,14 +36,21 @@ class RulesEngine:
def __init__(self, *rules: Rule) -> None:
self.rules = rules

def _get_message(self, rule) -> Optional[str]:
if rule.message:
return rule.message
return rule.condition.__name__ if rule.condition.__name__ != "<lambda>" else None

def run(self, *args: Any, **kwargs: Any) -> Any:
for rule in self.rules:
if rule.condition(*args, **kwargs):
return rule.action(*args, **kwargs, message=rule.message)
return Result(value=rule.action(*args, **kwargs), message=self._get_message(rule))

return Result(value=None, message="No conditions matched")

def run_all(self, *args: Any, **kwargs: Any) -> list:
return [
rule.action(*args, **kwargs, message=rule.message)
Result(value=rule.action(*args, **kwargs), message=self._get_message(rule))
for rule in self.rules
if rule.condition(*args, **kwargs)
]
Expand Down
57 changes: 37 additions & 20 deletions tests/test_article_completed_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pytest

from src.rules_engine import Otherwise, Rule, RulesEngine, not_, then
from src.rules_engine import Otherwise, Rule, RulesEngine, not_, then, Result

Article = namedtuple("Article", "title price image_url stock")

Expand All @@ -19,68 +19,85 @@ def article_image_missing(article):
return not article.image_url


def return_false_and_message(article, message):
return False, message


@pytest.mark.parametrize(
"article, result",
"article, expected_result, message",
[
(
Article(
title="Iphone Case", price=1000, image_url="http://localhost/image", stock=None
),
(False, "article stock missing"),
False,
"article stock missing",
),
(
Article(title="Iphone Case", price=None, image_url="http://image", stock=10),
False,
"article_price_missing",
),
(
Article(title="Iphone Case", price=1000, image_url="", stock=10),
False,
"article_image_missing",
),
(
Article(title="Iphone Case", price=1000, image_url="http://image", stock=10),
True,
None,
),
],
)
def test_article_complete_rules(article, result):
assert result == RulesEngine(
Rule(article_stock_missing, return_false_and_message, message="article stock missing"),
def test_article_complete_rules(article, expected_result, message):
result = RulesEngine(
Rule(article_stock_missing, then(False), message="article stock missing"),
Rule(article_price_missing, then(False)),
Rule(article_image_missing, then(False)),
Otherwise(then(True)),
).run(article)

result.value = expected_result
result.message = message


@pytest.mark.parametrize(
"article, result",
"article, expected_result",
[
(
Article(
title="Iphone Case", price=1000, image_url="http://localhost/image", stock=None
),
["B", "C"],
[
Result(value='B', message='article price missing'),
Result(value='C', message='article image missing'),
],
),
(
Article(title="Iphone Case", price=None, image_url="http://image", stock=10),
["A", "C"],
[
Result(value='A', message='article stock missing'),
Result(value='C', message='article image missing'),
],
),
(
Article(title="Iphone Case", price=1000, image_url="", stock=10),
["A", "B"],
[
Result(value='A', message='article stock missing'),
Result(value='B', message='article price missing'),
],
),
(
Article(title="Iphone Case", price=1000, image_url="http://image", stock=10),
["A", "B", "C"],
[
Result(value='A', message='article stock missing'),
Result(value='B', message='article price missing'),
Result(value='C', message='article image missing'),
],
),
],
)
def test_article_complete_all_rules(article, result):
assert result == RulesEngine(
Rule(not_(article_stock_missing), then("A")),
Rule(not_(article_price_missing), then("B")),
Rule(not_(article_image_missing), then("C")),
def test_article_complete_all_rules(article, expected_result):
result = RulesEngine(
Rule(not_(article_stock_missing), then("A"), message="article stock missing"),
Rule(not_(article_price_missing), then("B"), message="article price missing"),
Rule(not_(article_image_missing), then("C"), message="article image missing"),
).run_all(article)
assert result == expected_result
26 changes: 15 additions & 11 deletions tests/test_operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from src.rules_engine import Rule, RulesEngine, all_, any_, not_, then, when


def raise_cannot_be_none_error(obj, message):
def raise_cannot_be_none_error(obj):
raise ValueError("cannot be None error")


Expand All @@ -13,7 +13,9 @@ def test_when_then_operator():
with pytest.raises(ValueError):
RulesEngine(Rule(when(obj is None), raise_cannot_be_none_error)).run(obj)

assert RulesEngine(Rule(when(obj is not None), then(True))).run(obj) is None
result = RulesEngine(Rule(when(obj is not None), then(True), "obj is None")).run(obj)
assert result.value is None
assert result.message == "No conditions matched"


@pytest.mark.parametrize(
Expand All @@ -27,7 +29,7 @@ def test_when_then_operator():
def test_not_operator(condition, action, result):
obj = None

assert RulesEngine(Rule(not_(when(condition)), then(action))).run(obj) is result
assert RulesEngine(Rule(not_(when(condition)), then(action))).run(obj).value is result


@pytest.mark.parametrize(
Expand All @@ -42,19 +44,21 @@ def test_not_operator(condition, action, result):
def test_any_operator(conditions, action, result):
obj = None

assert RulesEngine(Rule(any_(*conditions), then(action))).run(obj) is result
assert RulesEngine(Rule(any_(*conditions), then(action))).run(obj).value is result


@pytest.mark.parametrize(
"conditions,action,result",
"conditions,action,value,message",
[
([when(False), when(False), when(False)], "A", None),
([when(True), when(False), when(False)], "A", None),
([when(True), when(True), when(False)], "A", None),
([when(True), when(True), when(True)], "A", "A"),
([when(False), when(False), when(False)], "A", None, "No conditions matched"),
([when(True), when(False), when(False)], "A", None, "No conditions matched"),
([when(True), when(True), when(False)], "A", None, "No conditions matched"),
([when(True), when(True), when(True)], "A", "A", None),
],
)
def test_all_operator(conditions, action, result):
def test_all_operator(conditions, action, value, message):
obj = None

assert RulesEngine(Rule(all_(*conditions), then(action))).run(obj) is result
result = RulesEngine(Rule(all_(*conditions), then(action))).run(obj)
assert result.value == value
assert result.message == message

0 comments on commit 1e28a08

Please sign in to comment.