Skip to content

Commit 19b3dba

Browse files
committed
business-rules extension updates
1 parent 3cd684b commit 19b3dba

File tree

2 files changed

+127
-117
lines changed

2 files changed

+127
-117
lines changed

business_rules/engine.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,16 @@ def check_conditions_recursively(conditions, defined_variables):
2929
return not check_conditions_recursively(conditions['not'], defined_variables)
3030

3131
if keys == ['all']:
32-
assert len(conditions['all']) >= 1
32+
if len(conditions['all']) < 1:
33+
raise ValueError("'all' conditions must have at least one child condition")
3334
for condition in conditions['all']:
3435
if not check_conditions_recursively(condition, defined_variables):
3536
return False
3637
return True
3738

3839
elif keys == ['any']:
39-
assert len(conditions['any']) >= 1
40+
if len(conditions['any']) < 1:
41+
raise ValueError("'any' conditions must have at least one child condition")
4042
for condition in conditions['any']:
4143
if check_conditions_recursively(condition, defined_variables):
4244
return True
@@ -54,9 +56,15 @@ def check_condition(condition, defined_variables):
5456
variables, values, and the comparison operator. The defined_variables
5557
object must have a variable defined for any variables in this condition.
5658
"""
57-
name, op, value = condition['name'], condition['operator'], condition['value']
59+
name, op, value, other_name = condition['name'], condition['operator'], condition.get('value'), condition.get('other_name')
5860
operator_type = _get_variable_value(defined_variables, name)
59-
return _do_operator_comparison(operator_type, op, value)
61+
if other_name:
62+
other_operator_type = _get_variable_value(defined_variables, other_name)
63+
if other_operator_type != operator_type:
64+
raise ValueError("Both variables in a comparison must be of the same type")
65+
return _do_operator_comparison(operator_type, op, other_operator_type)
66+
else:
67+
return _do_operator_comparison(operator_type, op, value)
6068

6169
def _get_variable_value(defined_variables, name):
6270
""" Call the function provided on the defined_variables object with the

tests/test_engine_logic.py

+115-113
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from unittest import TestCase
21
from unittest.mock import patch, MagicMock
32

43
import pytest
@@ -9,113 +8,106 @@
98
from business_rules.actions import BaseActions
109

1110

12-
class EngineTests(TestCase):
13-
###
14-
### Run
15-
###
16-
17-
@patch.object(engine, "run")
18-
def test_run_all_some_rule_triggered(self, *args):
19-
"""By default, does not stop on first triggered rule. Returns True if
20-
any rule was triggered, otherwise False
21-
"""
22-
rule1 = {"conditions": "condition1", "actions": "action name 1"}
23-
rule2 = {"conditions": "condition2", "actions": "action name 2"}
24-
variables = BaseVariables()
25-
actions = BaseActions()
26-
27-
def return_action1(rule, *args, **kwargs):
28-
return rule["actions"] == "action name 1"
29-
30-
engine.run.side_effect = return_action1
31-
32-
result = engine.run_all([rule1, rule2], variables, actions)
33-
self.assertTrue(result)
34-
self.assertEqual(engine.run.call_count, 2)
35-
36-
# switch order and try again
37-
engine.run.reset_mock()
38-
39-
result = engine.run_all([rule2, rule1], variables, actions)
40-
self.assertTrue(result)
41-
self.assertEqual(engine.run.call_count, 2)
42-
43-
@patch.object(engine, "run", return_value=True)
44-
def test_run_all_stop_on_first(self, *args):
45-
rule1 = {"conditions": "condition1", "actions": "action name 1"}
46-
rule2 = {"conditions": "condition2", "actions": "action name 2"}
47-
variables = BaseVariables()
48-
actions = BaseActions()
49-
50-
result = engine.run_all(
51-
[rule1, rule2], variables, actions, stop_on_first_trigger=True
52-
)
53-
self.assertEqual(result, True)
54-
self.assertEqual(engine.run.call_count, 1)
55-
engine.run.assert_called_once_with(rule1, variables, actions)
56-
57-
@patch.object(engine, "check_conditions_recursively", return_value=True)
58-
@patch.object(engine, "do_actions")
59-
def test_run_that_triggers_rule(self, *args):
60-
rule = {"conditions": "blah", "actions": "blah2"}
61-
variables = BaseVariables()
62-
actions = BaseActions()
63-
64-
result = engine.run(rule, variables, actions)
65-
self.assertEqual(result, True)
66-
engine.check_conditions_recursively.assert_called_once_with(
67-
rule["conditions"], variables
68-
)
69-
engine.do_actions.assert_called_once_with(rule["actions"], actions)
70-
71-
@patch.object(engine, "check_conditions_recursively", return_value=False)
72-
@patch.object(engine, "do_actions")
73-
def test_run_that_doesnt_trigger_rule(self, *args):
74-
rule = {"conditions": "blah", "actions": "blah2"}
75-
variables = BaseVariables()
76-
actions = BaseActions()
77-
78-
result = engine.run(rule, variables, actions)
79-
self.assertEqual(result, False)
80-
engine.check_conditions_recursively.assert_called_once_with(
81-
rule["conditions"], variables
82-
)
83-
self.assertEqual(engine.do_actions.call_count, 0)
84-
85-
###
86-
### Operator comparisons
87-
###
88-
def test_check_operator_comparison(self):
89-
string_type = StringType("yo yo")
90-
with patch.object(string_type, "contains", return_value=True):
91-
result = engine._do_operator_comparison(
92-
string_type, "contains", "its mocked"
93-
)
94-
self.assertTrue(result)
95-
string_type.contains.assert_called_once_with("its mocked")
96-
97-
###
98-
### Actions
99-
###
100-
def test_do_actions(self):
101-
actions = [
102-
{"name": "action1"},
103-
{"name": "action2", "params": {"param1": "foo", "param2": 10}},
104-
]
105-
defined_actions = BaseActions()
106-
defined_actions.action1 = MagicMock()
107-
defined_actions.action2 = MagicMock()
108-
109-
engine.do_actions(actions, defined_actions)
110-
111-
defined_actions.action1.assert_called_once_with()
112-
defined_actions.action2.assert_called_once_with(param1="foo", param2=10)
113-
114-
def test_do_with_invalid_action(self):
115-
actions = [{"name": "fakeone"}]
116-
err_string = "Action fakeone is not defined in class BaseActions"
117-
with self.assertRaisesRegex(AssertionError, err_string):
118-
engine.do_actions(actions, BaseActions())
11+
@patch.object(engine, "run")
12+
def test_run_all_some_rule_triggered(_mock_engine_run: MagicMock) -> None:
13+
"""By default, does not stop on first triggered rule. Returns True if
14+
any rule was triggered, otherwise False
15+
"""
16+
rule1 = {"conditions": "condition1", "actions": "action name 1"}
17+
rule2 = {"conditions": "condition2", "actions": "action name 2"}
18+
variables = BaseVariables()
19+
actions = BaseActions()
20+
21+
def return_action1(rule, *args, **kwargs):
22+
return rule["actions"] == "action name 1"
23+
24+
engine.run.side_effect = return_action1
25+
26+
result = engine.run_all([rule1, rule2], variables, actions)
27+
assert result is True
28+
assert engine.run.call_count == 2
29+
30+
# switch order and try again
31+
engine.run.reset_mock()
32+
33+
result = engine.run_all([rule2, rule1], variables, actions)
34+
assert result is True
35+
assert engine.run.call_count == 2
36+
37+
38+
@patch.object(engine, "run", return_value=True)
39+
def test_run_all_stop_on_first(_mock_engine_run: MagicMock) -> None:
40+
rule1 = {"conditions": "condition1", "actions": "action name 1"}
41+
rule2 = {"conditions": "condition2", "actions": "action name 2"}
42+
variables = BaseVariables()
43+
actions = BaseActions()
44+
45+
result = engine.run_all(
46+
[rule1, rule2], variables, actions, stop_on_first_trigger=True
47+
)
48+
assert result is True
49+
assert engine.run.call_count == 1
50+
engine.run.assert_called_once_with(rule1, variables, actions)
51+
52+
53+
@patch.object(engine, "check_conditions_recursively", return_value=True)
54+
@patch.object(engine, "do_actions")
55+
def test_run_that_triggers_rule(
56+
_mock_do_actions: MagicMock, _mock_check_conditions_recursively: MagicMock
57+
) -> None:
58+
rule = {"conditions": "blah", "actions": "blah2"}
59+
variables = BaseVariables()
60+
actions = BaseActions()
61+
62+
result = engine.run(rule, variables, actions)
63+
assert result is True
64+
engine.check_conditions_recursively.assert_called_once_with(
65+
rule["conditions"], variables
66+
)
67+
engine.do_actions.assert_called_once_with(rule["actions"], actions)
68+
69+
70+
@patch.object(engine, "check_conditions_recursively", return_value=False)
71+
@patch.object(engine, "do_actions")
72+
def test_run_that_doesnt_trigger_rule(
73+
_mock_do_actions: MagicMock, _mock_check_conditions_recursively: MagicMock
74+
) -> None:
75+
"""
76+
DOCUMENT ME.
77+
"""
78+
rule = {"conditions": "blah", "actions": "blah2"}
79+
variables = BaseVariables()
80+
actions = BaseActions()
81+
# set up
82+
result = engine.run(rule, variables, actions)
83+
assert result is False
84+
engine.check_conditions_recursively.assert_called_once_with(
85+
rule["conditions"], variables
86+
)
87+
assert engine.do_actions.call_count == 0
88+
89+
90+
def test_do_actions() -> None:
91+
actions = [
92+
{"name": "action1"},
93+
{"name": "action2", "params": {"param1": "foo", "param2": 10}},
94+
]
95+
defined_actions = BaseActions()
96+
defined_actions.action1 = MagicMock()
97+
defined_actions.action2 = MagicMock()
98+
99+
engine.do_actions(actions, defined_actions)
100+
101+
defined_actions.action1.assert_called_once_with()
102+
defined_actions.action2.assert_called_once_with(param1="foo", param2=10)
103+
104+
105+
def test_check_operator_comparison() -> None:
106+
string_type = StringType("yo yo")
107+
with patch.object(string_type, "contains", return_value=True):
108+
result = engine._do_operator_comparison(string_type, "contains", "its mocked")
109+
assert result is True
110+
string_type.contains.assert_called_once_with("its mocked")
119111

120112

121113
@patch.object(engine, "check_condition", return_value=True)
@@ -138,7 +130,6 @@ def test_check_all_conditions_with_all_false(_mock_check_condition: MagicMock) -
138130
variables = BaseVariables()
139131
# test
140132
result = engine.check_conditions_recursively(conditions, variables)
141-
# self.assertEqual(result, False)
142133
assert result is False
143134
engine.check_condition.assert_called_once_with({"thing1": ""}, variables)
144135

@@ -154,7 +145,9 @@ def test_check_all_and_any_together() -> None:
154145

155146

156147
def test_check_all_conditions_with_no_items_fails() -> None:
157-
with pytest.raises(AssertionError):
148+
with pytest.raises(
149+
ValueError, match="'all' conditions must have at least one child condition"
150+
):
158151
engine.check_conditions_recursively({"all": []}, BaseVariables())
159152

160153

@@ -181,22 +174,24 @@ def test_check_any_conditions_with_all_false(_mock_check_condition: MagicMock) -
181174

182175

183176
def test_check_any_condition_with_no_items_fails() -> None:
184-
with pytest.raises(AssertionError):
177+
with pytest.raises(
178+
ValueError, match="'any' conditions must have at least one child condition"
179+
):
185180
engine.check_conditions_recursively({"any": []}, BaseVariables())
186181

187182

188183
@patch.object(engine, "check_condition")
189184
def test_nested_all_and_any(_mock_check_condition: MagicMock) -> None:
185+
# test
190186
conditions = {"all": [{"any": [{"name": 1}, {"name": 2}]}, {"name": 3}]}
191187
bv = BaseVariables()
192188

193189
def side_effect(condition, _):
194190
return condition["name"] in [2, 3]
195191

196192
engine.check_condition.side_effect = side_effect
197-
193+
# test
198194
engine.check_conditions_recursively(conditions, bv)
199-
# self.assertEqual(engine.check_condition.call_count, 3)
200195
assert engine.check_condition.call_count == 3
201196
engine.check_condition.assert_any_call({"name": 1}, bv)
202197
engine.check_condition.assert_any_call({"name": 2}, bv)
@@ -235,3 +230,10 @@ def test_check_not_negates_result(
235230
result = engine.check_conditions_recursively(conditions, variables)
236231
assert result is expected_result
237232
mock_check_condition.assert_called_once_with({"thing1": ""}, variables)
233+
234+
235+
def test_do_with_invalid_action() -> None:
236+
actions = [{"name": "fakeone"}]
237+
err_string = "Action fakeone is not defined in class BaseActions"
238+
with pytest.raises(AssertionError, match=err_string):
239+
engine.do_actions(actions, BaseActions())

0 commit comments

Comments
 (0)