Skip to content

Commit

Permalink
chore: Add Translator unit tests (#17)
Browse files Browse the repository at this point in the history
* chore: Add Rule Generator unit tests

* chore: Fix formatting

* chore: Add more Rule Generator unit tests

* chore: Make rule predicate a constant

* chore: Add Eval Translator unit tests
  • Loading branch information
barreeeiroo authored Jan 1, 2024
1 parent 94f2366 commit 231d627
Show file tree
Hide file tree
Showing 7 changed files with 410 additions and 14 deletions.
9 changes: 6 additions & 3 deletions json_logic_asp/adapters/asp/asp_statements.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import List
from typing import List, Union

from json_logic_asp.adapters.asp.asp_literals import Literal, PredicateAtom
from json_logic_asp.constants.asp_naming import PredicateNames
from json_logic_asp.models.asp_base import Statement


Expand Down Expand Up @@ -34,5 +35,7 @@ def to_asp_statement(self):


class ShowStatement(DirectiveStatement):
def __init__(self, statement: str, length: int):
super().__init__(action="show", statement=f"{statement}/{length}")
def __init__(self, predicate: Union[str, PredicateNames], length: int):
if isinstance(predicate, PredicateNames):
predicate = predicate.value
super().__init__(action="show", statement=f"{predicate}/{length}")
2 changes: 2 additions & 0 deletions json_logic_asp/constants/asp_naming.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class PredicateNames(str, Enum):
LOGIC_GREATER = "gt"
LOGIC_GREATEREQUAL = "gte"

RULE = "rule"


class VariableNames(str, Enum):
ANY = "_"
Expand Down
33 changes: 24 additions & 9 deletions json_logic_asp/translator/eval_translator.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,59 @@
from typing import List
from typing import Dict, List, Optional, Type

from json_logic_asp.adapters.asp.asp_statements import ShowStatement
from json_logic_asp.constants.asp_naming import PredicateNames
from json_logic_asp.models.translator_dto import DataInput, RuleInput, RuleOutput
from json_logic_asp.translator.data_generator import generate_single_data_asp_definition
from json_logic_asp.translator.rule_generator import generate_multiple_rule_asp_definition


def translate_multi_rule_eval(
rule_inputs: List[RuleInput], data_input: DataInput, with_comments: bool = False
rule_inputs: List[RuleInput],
data_input: DataInput,
with_comments: bool = False,
custom_nodes: Optional[Dict[str, Type]] = None,
) -> RuleOutput:
"""
Given some rule inputs and a data input, generate the corresponding ASP definition ready to be executed.
:param rule_inputs: multiple rule input objects with the rule definitions
:param data_input: single data input with data to be evaluated against the rules
:param with_comments: whether the returning ASP statements should include ASP comments
:param custom_nodes: optional dictionary of custom nodes to support in the rule translation
:return: data object with the generated ASP definition
"""
stmts: List[str] = []

data_str = generate_single_data_asp_definition(data_input, with_comments=with_comments)
rule_str, rule_mapping = generate_multiple_rule_asp_definition(rule_inputs, with_comments=with_comments)
data_str = generate_single_data_asp_definition(data_input=data_input, with_comments=with_comments)
rule_str, rule_mapping = generate_multiple_rule_asp_definition(
rule_inputs=rule_inputs, with_comments=with_comments, custom_nodes=custom_nodes
)

stmts.append(data_str)
stmts.extend(data_str.split("\n"))
stmts.append("")
stmts.append(rule_str)
stmts.extend(rule_str.split("\n"))
stmts.append("")
stmts.append(ShowStatement("rule", 1).to_asp_statement())
stmts.append(ShowStatement(PredicateNames.RULE, 1).to_asp_statement())

return RuleOutput(
statements=stmts,
rule_mapping=rule_mapping,
)


def translate_single_rule_eval(rule_input: RuleInput, data_input: DataInput, with_comments: bool = False) -> RuleOutput:
def translate_single_rule_eval(
rule_input: RuleInput,
data_input: DataInput,
with_comments: bool = False,
custom_nodes: Optional[Dict[str, Type]] = None,
) -> RuleOutput:
"""
Given a rule input and a data input, generate the corresponding ASP definition ready to be executed.
:param rule_input: single rule input objects with the rule definition
:param data_input: single data input with data to be evaluated against the rules
:param with_comments: whether the returning ASP statements should include ASP comments
:param custom_nodes: optional dictionary of custom nodes to support in the rule translation
:return: data object with the generated ASP definition
"""
return translate_multi_rule_eval(rule_inputs=[rule_input], data_input=data_input, with_comments=with_comments)
return translate_multi_rule_eval(
rule_inputs=[rule_input], data_input=data_input, with_comments=with_comments, custom_nodes=custom_nodes
)
3 changes: 2 additions & 1 deletion json_logic_asp/translator/rule_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
LogicStrictEqualNode,
LogicStrictNotEqualNode,
)
from json_logic_asp.constants.asp_naming import PredicateNames
from json_logic_asp.constants.json_logic_ops import JsonLogicOps
from json_logic_asp.models.json_logic_nodes import JsonLogicNode
from json_logic_asp.models.translator_dto import RuleInput
Expand Down Expand Up @@ -134,7 +135,7 @@ def generate_multiple_rule_asp_definition(
mapping[hashed_id] = rule_input.rule_id

root_statement = RuleStatement(
atom=PredicateAtom(predicate_name="rule", terms=[hashed_id]),
atom=PredicateAtom(predicate_name=PredicateNames.RULE, terms=[hashed_id]),
literals=[root_node.get_asp_atom()],
comment=rule_input.rule_id,
)
Expand Down
7 changes: 6 additions & 1 deletion tests/test_adapters/test_asp/test_asp_statements.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from json_logic_asp.adapters.asp.asp_literals import PredicateAtom
from json_logic_asp.adapters.asp.asp_statements import DirectiveStatement, FactStatement, RuleStatement, ShowStatement
from json_logic_asp.constants.asp_naming import PredicateNames


class TestFactStatement:
Expand Down Expand Up @@ -36,5 +37,9 @@ def test_comments(self):

class TestShowStatement:
def test_statement(self):
stmt = ShowStatement(statement="test", length=2)
stmt = ShowStatement(predicate="test", length=2)
assert stmt.to_asp_statement() == "#show test/2."

def test_statement_enum(self):
stmt = ShowStatement(predicate=PredicateNames.RULE, length=1)
assert stmt.to_asp_statement() == "#show rule/1."
181 changes: 181 additions & 0 deletions tests/test_translator/test_eval_translator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
from unittest.mock import patch

import pytest

from json_logic_asp.models.translator_dto import DataInput, RuleInput, RuleOutput
from json_logic_asp.translator import translate_multi_rule_eval, translate_single_rule_eval
from tests.fixtures import cuid_fixture # noqa


@pytest.mark.parametrize(
"with_comments, custom_nodes",
[
(False, None),
(True, None),
(False, {"dummy": None}),
(True, {"dummy": None}),
],
ids=[
"simple",
"with_comments",
"custom_nodes",
"both",
],
)
@patch("json_logic_asp.translator.eval_translator.generate_multiple_rule_asp_definition")
@patch("json_logic_asp.translator.eval_translator.generate_single_data_asp_definition")
def test_translate_multi_rule_eval(
mock_generate_single_data_asp_definition, mock_generate_multiple_rule_asp_definition, with_comments, custom_nodes
):
mock_generate_single_data_asp_definition.return_value = "var1\nvar2\nvar3"
mock_generate_multiple_rule_asp_definition.return_value = "stmt1\nstmt2\nstmt3", {"a": "b", "c": "d"}

ri1 = RuleInput(
rule_id="test1",
rule_tree={"and": {"==": [{"var": "a"}, "b"]}},
)
ri2 = RuleInput(
rule_id="test2",
rule_tree={"or": {">": [{"var": "c"}, "d"]}},
)
di = DataInput(
data_id="b",
data_object={
"e": "f",
"g": {"h": "i"},
},
)
rule_output = translate_multi_rule_eval([ri1, ri2], di, with_comments=with_comments, custom_nodes=custom_nodes)

assert rule_output.statements == ["var1", "var2", "var3", "", "stmt1", "stmt2", "stmt3", "", "#show rule/1."]
assert rule_output.rule_mapping == {"a": "b", "c": "d"}
mock_generate_single_data_asp_definition.assert_called_once_with(
data_input=di,
with_comments=with_comments,
)
mock_generate_multiple_rule_asp_definition.assert_called_once_with(
rule_inputs=[ri1, ri2],
with_comments=with_comments,
custom_nodes=custom_nodes,
)


def test_translate_multi_rule_eval_simple():
ri1 = RuleInput(
rule_id="test1",
rule_tree={"and": {"==": [{"var": "a"}, "b"]}},
)
ri2 = RuleInput(
rule_id="test2",
rule_tree={"or": {">": [{"var": "c"}, "d"]}},
)
di = DataInput(
data_id="e",
data_object={
"f": "g",
"h": {"i": "j"},
},
)

rule_output = translate_multi_rule_eval([ri1, ri2], di, with_comments=True)

assert rule_output.statements == [
"% f : g",
"var(s8fa14cdd754f91cc6554c9e71929cce7, sb2f5ff47436671b6e533d8dc3614845d).",
"% h.i : j",
"var(sd95e8ab9a13affcd43b13b0b5443d484, s363b122c528f54df4a0446b6bab05515).",
"",
"% a EQ b",
"eq(mock2) :- var(s0cc175b9c0f1b6a831c399e269772661, V1), V1 == s92eb5ffee6ae2fec3ad71c777531578f.",
"and(mock3) :- eq(mock2).",
"% c GT d",
"gt(mock5) :- var(s4a8a08f09d37b73795649038408b5f33, V1), V1 > s8277e0910d750195b448797616e091ad.",
"or(mock6) :- gt(mock5).",
"% test1",
"rule(s5a105e8b9d40e1329780d62ea2265d8a) :- and(mock3).",
"% test2",
"rule(sad0234829205b9033196ba818f7a872b) :- or(mock6).",
"",
"#show rule/1.",
]
assert rule_output.rule_mapping == {
"s5a105e8b9d40e1329780d62ea2265d8a": "test1",
"sad0234829205b9033196ba818f7a872b": "test2",
}


@pytest.mark.parametrize(
"with_comments, custom_nodes",
[
(False, None),
(True, None),
(False, {"dummy": None}),
(True, {"dummy": None}),
],
ids=[
"simple",
"with_comments",
"custom_nodes",
"both",
],
)
@patch("json_logic_asp.translator.eval_translator.translate_multi_rule_eval")
def test_translate_single_rule_eval(mock_translate_multi_rule_eval, with_comments, custom_nodes):
mock_ro = RuleOutput(
statements=[],
rule_mapping={},
)
mock_translate_multi_rule_eval.return_value = mock_ro

ri = RuleInput(
rule_id="a",
rule_tree={"c": "d"},
)
di = DataInput(
data_id="b",
data_object={
"e": "f",
"g": {"h": "i"},
},
)
rule_output = translate_single_rule_eval(ri, di, with_comments=with_comments, custom_nodes=custom_nodes)

assert rule_output == mock_ro
mock_translate_multi_rule_eval.assert_called_once_with(
rule_inputs=[ri],
data_input=di,
with_comments=with_comments,
custom_nodes=custom_nodes,
)


def test_translate_single_rule_eval_simple():
ri = RuleInput(
rule_id="test",
rule_tree={"and": {"==": [{"var": "a"}, "b"]}},
)
di = DataInput(
data_id="b",
data_object={
"e": "f",
"g": {"h": "i"},
},
)

rule_output = translate_single_rule_eval(ri, di, with_comments=True)

assert rule_output.statements == [
"% e : f",
"var(se1671797c52e15f763380b45e841ec32, s8fa14cdd754f91cc6554c9e71929cce7).",
"% g.h : i",
"var(s08a6c56ffa6dc13f368e7e73c0ca58ec, s865c0c0b4ab0e063e5caa3387c1a8741).",
"",
"% a EQ b",
"eq(mock2) :- var(s0cc175b9c0f1b6a831c399e269772661, V1), V1 == s92eb5ffee6ae2fec3ad71c777531578f.",
"and(mock3) :- eq(mock2).",
"% test",
"rule(s098f6bcd4621d373cade4e832627b4f6) :- and(mock3).",
"",
"#show rule/1.",
]
assert rule_output.rule_mapping == {"s098f6bcd4621d373cade4e832627b4f6": "test"}
Loading

0 comments on commit 231d627

Please sign in to comment.