Skip to content

Commit

Permalink
chore: Boolean Node unit tests (#16)
Browse files Browse the repository at this point in the history
* chore: Improve MyPY checks

* chore: Add Boolean Node unit tests
  • Loading branch information
barreeeiroo authored Jan 1, 2024
1 parent 630cfd3 commit 94f2366
Show file tree
Hide file tree
Showing 9 changed files with 305 additions and 32 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ lint-strict: format
poetry run ruff check json_logic_asp/ tests/

mypy:
poetry run mypy --pretty json_logic_asp/ tests/
poetry run mypy --pretty --check-untyped-def json_logic_asp/ tests/

test:
poetry run pytest
Expand Down
86 changes: 62 additions & 24 deletions json_logic_asp/adapters/json_logic/jl_boolean_nodes.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,50 @@
from abc import ABC
from typing import Dict, List, Optional

from json_logic_asp.adapters.asp.asp_literals import PredicateAtom
from json_logic_asp.adapters.asp.asp_statements import RuleStatement
from json_logic_asp.adapters.asp.asp_statements import FactStatement, RuleStatement
from json_logic_asp.adapters.json_logic.jl_data_nodes import DataVarNode
from json_logic_asp.constants.asp_naming import PredicateNames, VariableNames
from json_logic_asp.models.asp_base import Statement
from json_logic_asp.models.json_logic_nodes import JsonLogicDataNode, JsonLogicTreeNode


class BooleanAndNode(JsonLogicTreeNode):
def __init__(self, *children):
super().__init__(operation_name=PredicateNames.BOOLEAN_AND)
class BooleanAndOrNode(JsonLogicTreeNode, ABC):
def __init__(self, *children, operation_name: str):
super().__init__(operation_name=operation_name)

if len(children) < 1:
raise ValueError(f"{self.__class__.__name__} requires at least 1 child")

self.has_true: bool = False
self.has_false: bool = False

for child_node in children:
if isinstance(child_node, bool):
self.has_false = self.has_false or child_node is False
self.has_true = self.has_true or child_node is True
continue

self.register_child(child_node)


class BooleanAndNode(BooleanAndOrNode):
def __init__(self, *children):
super().__init__(operation_name=PredicateNames.BOOLEAN_AND, *children)

def get_asp_statements(self) -> List[Statement]:
if self.has_false:
# If the condition can never be satisfied, return no statements (never satisfy the node)
return []

if self.has_true and not self.child_nodes:
# If any True present but no other children, then it's just a fact
return [
FactStatement(
atom=self.get_asp_atom(),
)
]

# For each child node, get the atom and use it as literal
child_statements: Dict[str, PredicateAtom] = {}
for child_node in self.child_nodes:
Expand All @@ -29,14 +58,21 @@ def get_asp_statements(self) -> List[Statement]:
]


class BooleanOrNode(JsonLogicTreeNode):
class BooleanOrNode(BooleanAndOrNode):
def __init__(self, *children):
super().__init__(operation_name=PredicateNames.BOOLEAN_OR)

for child_node in children:
self.register_child(child_node)
super().__init__(operation_name=PredicateNames.BOOLEAN_OR, *children)

def get_asp_statements(self) -> List[Statement]:
if self.has_true:
return [
FactStatement(
atom=self.get_asp_atom(),
)
]

if not self.child_nodes:
return []

stmts: List[Statement] = []

for child_node in self.child_nodes:
Expand All @@ -62,21 +98,23 @@ def get_asp_statements(self) -> List[Statement]:
# For each child node, get the atom and use it as literal
child_statements: Dict[str, PredicateAtom] = {}
comment: Optional[str] = None
for child_node in self.child_nodes:
if isinstance(child_node, JsonLogicDataNode):
# Handle specific case for negating "var" nodes as "not present"
var_name = child_node.var_name if isinstance(child_node, DataVarNode) else str(child_node)
child_statements[child_node.node_id] = child_node.get_asp_atom_with_different_variable_name(
VariableNames.ANY, negated=True
)
comment = f"Not {var_name}"
else:
child_atom = child_node.get_asp_atom()
child_statements[child_node.node_id] = PredicateAtom(
predicate_name=child_atom.predicate_name,
terms=child_atom.terms,
negated=not child_atom.negated,
)

child_node = self.child_nodes[0]

if isinstance(child_node, JsonLogicDataNode):
# Handle specific case for negating "var" nodes as "not present"
var_name = child_node.var_name if isinstance(child_node, DataVarNode) else str(child_node)
child_statements[child_node.node_id] = child_node.get_asp_atom_with_different_variable_name(
VariableNames.ANY, negated=True
)
comment = f"Not {var_name}"
else:
child_atom = child_node.get_asp_atom()
child_statements[child_node.node_id] = PredicateAtom(
predicate_name=child_atom.predicate_name,
terms=child_atom.terms,
negated=not child_atom.negated,
)

return [
RuleStatement(
Expand Down
2 changes: 1 addition & 1 deletion json_logic_asp/adapters/json_logic/jl_data_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def __init__(self, *children):
super().__init__(operation_name=PredicateNames.DATA_MISSING)

if len(children) < 1:
raise ValueError(f"DataVarNode requires at least 1 child, received {len(children)}")
raise ValueError("DataVarNode requires at least 1 child")

self.var_names: List[str] = []
for var_name in children:
Expand Down
2 changes: 1 addition & 1 deletion json_logic_asp/adapters/json_logic/jl_logic_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def __init__(self, *children):
)

if len(children) < 1:
raise ValueError(f"LogicIfNode at least 1 child, received {len(children)}")
raise ValueError("LogicIfNode at least 1 child")

for child_node in children:
self.register_child(child_node)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_adapters/test_json_logic/test_jl_array_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def test_invalid_values(self):
assert exc1.match("Found unexpected child_node type NoneType for ArrayMergeNode")

with pytest.raises(ValueError) as exc2:
ArrayMergeNode(BooleanAndNode())
ArrayMergeNode(BooleanAndNode(True))
assert exc2.match("Found unexpected child_node type BooleanAndNode for ArrayMergeNode")

with pytest.raises(ValueError) as exc3:
Expand Down
231 changes: 231 additions & 0 deletions tests/test_adapters/test_json_logic/test_jl_boolean_nodes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import pytest

from json_logic_asp.adapters.json_logic.jl_boolean_nodes import BooleanAndNode, BooleanNotNode, BooleanOrNode
from json_logic_asp.adapters.json_logic.jl_data_nodes import DataVarNode
from json_logic_asp.adapters.json_logic.jl_logic_nodes import LogicEqualNode


class TestBooleanAndNode:
def test_invalid_values(self):
with pytest.raises(ValueError) as exc1:
BooleanAndNode()
assert exc1.match("BooleanAndNode requires at least 1 child")

with pytest.raises(ValueError) as exc2:
BooleanAndNode("a")
assert exc2.match("Found unexpected child_node type str for BooleanAndNode")

def test_valid_values(self):
node = BooleanAndNode(True)
assert isinstance(node, BooleanAndNode) and not node.has_false

node = BooleanAndNode(False)
assert isinstance(node, BooleanAndNode) and node.has_false

def test_statements(self):
eq1 = LogicEqualNode("a", "b")
eq2 = LogicEqualNode("c", "d")
node = BooleanAndNode(eq1, eq2)

assert node.to_asp(with_comment=True) == [
"% a EQ b",
"eq(mock1) :- s0cc175b9c0f1b6a831c399e269772661 == s92eb5ffee6ae2fec3ad71c777531578f.",
"% c EQ d",
"eq(mock2) :- s4a8a08f09d37b73795649038408b5f33 == s8277e0910d750195b448797616e091ad.",
"and(mock3) :- eq(mock1), eq(mock2).",
]

def test_statements_true(self):
eq1 = LogicEqualNode("a", "b")
eq2 = LogicEqualNode("c", "d")
node = BooleanAndNode(eq1, True, eq2)

assert node.to_asp(with_comment=True) == [
"% a EQ b",
"eq(mock1) :- s0cc175b9c0f1b6a831c399e269772661 == s92eb5ffee6ae2fec3ad71c777531578f.",
"% c EQ d",
"eq(mock2) :- s4a8a08f09d37b73795649038408b5f33 == s8277e0910d750195b448797616e091ad.",
"and(mock3) :- eq(mock1), eq(mock2).",
]

def test_statements_false(self):
eq1 = LogicEqualNode("a", "b")
eq2 = LogicEqualNode("c", "d")
node = BooleanAndNode(eq1, False, eq2)

assert node.to_asp(with_comment=True) == [
"% a EQ b",
"eq(mock1) :- s0cc175b9c0f1b6a831c399e269772661 == s92eb5ffee6ae2fec3ad71c777531578f.",
"% c EQ d",
"eq(mock2) :- s4a8a08f09d37b73795649038408b5f33 == s8277e0910d750195b448797616e091ad.",
]

def test_statements_empty_true(self):
node = BooleanAndNode(True, True)

assert node.to_asp(with_comment=True) == ["and(mock1)."]

def test_statements_empty_false(self):
node = BooleanAndNode(False, False)

assert node.to_asp(with_comment=True) == []

def test_statements_empty_mix(self):
node = BooleanAndNode(True, False)

assert node.to_asp(with_comment=True) == []

def test_str(self):
node = BooleanAndNode(True)
assert str(node) == "AND(mock1)"

def test_hash(self):
eq1 = LogicEqualNode("a", "b")
eq1b = LogicEqualNode("b", "a")
eq2 = LogicEqualNode("c", "d")
eq3 = LogicEqualNode("e", "f")
node1 = BooleanAndNode(eq1, eq2)
node2 = BooleanAndNode(eq2, eq1b, eq1)
node3 = BooleanAndNode(eq2, eq1, eq3)

assert hash(node1) == hash(("and", hash(tuple(sorted([hash(eq1), hash(eq2)]))))) == hash(node2) != hash(node3)
assert node1 == node2 != node3


class TestBooleanOrNode:
def test_invalid_values(self):
with pytest.raises(ValueError) as exc1:
BooleanOrNode()
assert exc1.match("BooleanOrNode requires at least 1 child")

with pytest.raises(ValueError) as exc2:
BooleanOrNode("a")
assert exc2.match("Found unexpected child_node type str for BooleanOrNode")

def test_valid_values(self):
node = BooleanOrNode(True)
assert isinstance(node, BooleanOrNode) and node.has_true

node = BooleanOrNode(False)
assert isinstance(node, BooleanOrNode) and not node.has_true

def test_statements(self):
eq1 = LogicEqualNode("a", "b")
eq2 = LogicEqualNode("c", "d")
node = BooleanOrNode(eq1, eq2)

assert node.to_asp(with_comment=True) == [
"% a EQ b",
"eq(mock1) :- s0cc175b9c0f1b6a831c399e269772661 == s92eb5ffee6ae2fec3ad71c777531578f.",
"% c EQ d",
"eq(mock2) :- s4a8a08f09d37b73795649038408b5f33 == s8277e0910d750195b448797616e091ad.",
"or(mock3) :- eq(mock1).",
"or(mock3) :- eq(mock2).",
]

def test_statements_true(self):
eq1 = LogicEqualNode("a", "b")
eq2 = LogicEqualNode("c", "d")
node = BooleanOrNode(eq1, True, eq2)

assert node.to_asp(with_comment=True) == [
"% a EQ b",
"eq(mock1) :- s0cc175b9c0f1b6a831c399e269772661 == s92eb5ffee6ae2fec3ad71c777531578f.",
"% c EQ d",
"eq(mock2) :- s4a8a08f09d37b73795649038408b5f33 == s8277e0910d750195b448797616e091ad.",
"or(mock3).",
]

def test_statements_false(self):
eq1 = LogicEqualNode("a", "b")
eq2 = LogicEqualNode("c", "d")
node = BooleanOrNode(eq1, False, eq2)

assert node.to_asp(with_comment=True) == [
"% a EQ b",
"eq(mock1) :- s0cc175b9c0f1b6a831c399e269772661 == s92eb5ffee6ae2fec3ad71c777531578f.",
"% c EQ d",
"eq(mock2) :- s4a8a08f09d37b73795649038408b5f33 == s8277e0910d750195b448797616e091ad.",
"or(mock3) :- eq(mock1).",
"or(mock3) :- eq(mock2).",
]

def test_statements_empty_true(self):
node = BooleanOrNode(True, True)

assert node.to_asp(with_comment=True) == ["or(mock1)."]

def test_statements_empty_false(self):
node = BooleanOrNode(False, False)

assert node.to_asp(with_comment=True) == []

def test_statements_empty_mix(self):
node = BooleanOrNode(True, False)

assert node.to_asp(with_comment=True) == ["or(mock1)."]

def test_str(self):
node = BooleanOrNode(True)
assert str(node) == "OR(mock1)"

def test_hash(self):
eq1 = LogicEqualNode("a", "b")
eq1b = LogicEqualNode("b", "a")
eq2 = LogicEqualNode("c", "d")
eq3 = LogicEqualNode("e", "f")
node1 = BooleanOrNode(eq1, eq2)
node2 = BooleanOrNode(eq2, eq1b, eq1)
node3 = BooleanOrNode(eq2, eq1, eq3)

assert hash(node1) == hash(("or", hash(tuple(sorted([hash(eq1), hash(eq2)]))))) == hash(node2) != hash(node3)
assert node1 == node2 != node3


class TestBooleanNotNode:
def test_invalid_values(self):
with pytest.raises(ValueError) as exc1:
BooleanNotNode()
assert exc1.match("BooleanNotNode expects only 1 child, received 0")

with pytest.raises(ValueError) as exc2:
BooleanNotNode("a", "b")
assert exc2.match("BooleanNotNode expects only 1 child, received 2")

def test_valid_values(self):
data_var = DataVarNode("data_var")
node = BooleanNotNode(data_var)
assert isinstance(node, BooleanNotNode)

def test_statements_data_node(self):
data_var = DataVarNode("data_var")
node = BooleanNotNode(data_var)

assert node.to_asp(with_comment=True) == [
"% Not data_var",
"neg(mock2) :- not var(s38bb977078c0e5ba5b0b759cf506cc4c, _).",
]

def test_statements_tree_nodes(self):
and_node = BooleanAndNode(True)
node = BooleanNotNode(and_node)

assert node.to_asp(with_comment=True) == [
"and(mock1).",
"neg(mock2) :- not and(mock1).",
]

def test_str(self):
data_var = DataVarNode("data_var")
node = BooleanNotNode(data_var)
assert str(node) == "NEG(mock2)"

def test_hash(self):
dv1 = DataVarNode("data_var1")
dv2 = DataVarNode("data_var2")
node1 = BooleanNotNode(dv1)
node2 = BooleanNotNode(dv1)
node3 = BooleanNotNode(dv2)

assert hash(node1) == hash(("neg", hash((hash(dv1),)))) == hash(node2) != hash(node3)
assert node1 == node2 != node3
4 changes: 4 additions & 0 deletions tests/test_models/test_json_logic_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,10 @@ def test_asp_atom_with_different_var_name(self, cls):
node = cls(operation_name="test", term_variable_name="T")
assert node.get_asp_atom_with_different_variable_name("V").to_asp_atom() == "test(mock1, V)"

def test_asp_atom_with_different_var_name_negated(self, cls):
node = cls(operation_name="test", term_variable_name="T")
assert node.get_asp_atom_with_different_variable_name("V", negated=True).to_asp_atom() == "not test(mock1, V)"

def test_to_asp(self, cls):
node = cls(operation_name="test2", term_variable_name="T")
assert node.to_asp() == ["test2(mock1, T)."]
Loading

0 comments on commit 94f2366

Please sign in to comment.