diff --git a/drools-ansible-rulebook-integration-api/src/main/java/org/drools/ansible/rulebook/integration/api/rulesengine/RegisterOnlyAgendaFilter.java b/drools-ansible-rulebook-integration-api/src/main/java/org/drools/ansible/rulebook/integration/api/rulesengine/RegisterOnlyAgendaFilter.java index 68afa419..9a6d9dff 100644 --- a/drools-ansible-rulebook-integration-api/src/main/java/org/drools/ansible/rulebook/integration/api/rulesengine/RegisterOnlyAgendaFilter.java +++ b/drools-ansible-rulebook-integration-api/src/main/java/org/drools/ansible/rulebook/integration/api/rulesengine/RegisterOnlyAgendaFilter.java @@ -91,6 +91,12 @@ private static boolean hasPartialMatch(InternalFactHandle fh) { private static boolean isPartialMatch(Tuple tuple) { // the tuple is a partial match if never reached a terminal node for (; tuple != null; tuple = tuple.getFirstChild()) { + if (tuple instanceof LeftTuple) { + LeftTuple peer = ((LeftTuple) tuple).getPeer(); + if (peer != null && isPartialMatch(peer)) { + return true; // if any peer is a partial match, this tuple is also a partial match + } + } if (tuple instanceof RuleTerminalNodeLeftTuple) { return false; } diff --git a/drools-ansible-rulebook-integration-api/src/test/java/org/drools/ansible/rulebook/integration/api/MultipleRuleMatchTest.java b/drools-ansible-rulebook-integration-api/src/test/java/org/drools/ansible/rulebook/integration/api/MultipleRuleMatchTest.java index e83c3368..887d9002 100644 --- a/drools-ansible-rulebook-integration-api/src/test/java/org/drools/ansible/rulebook/integration/api/MultipleRuleMatchTest.java +++ b/drools-ansible-rulebook-integration-api/src/test/java/org/drools/ansible/rulebook/integration/api/MultipleRuleMatchTest.java @@ -248,4 +248,399 @@ private static void checkPartialMatchesWithMatchMultipleRules(boolean matchMulti rulesExecutor.dispose(); } + + @Test + public void retainLeftPartialMatchesWithMatchMultipleRulesWithPeer() { + checkPartialMatchesWithMatchMultipleRulesWithPeer(true, true); + } + + @Test + public void discardLeftPartialMatchesWithMatchMultipleRulesWithPeer() { + checkPartialMatchesWithMatchMultipleRulesWithPeer(false, true); + } + + @Test + public void retainRightPartialMatchesWithMatchMultipleRulesWithPeer() { + checkPartialMatchesWithMatchMultipleRulesWithPeer(true, false); + } + + @Test + public void discardRightPartialMatchesWithMatchMultipleRulesWithPeer() { + checkPartialMatchesWithMatchMultipleRulesWithPeer(false, false); + } + + private static void checkPartialMatchesWithMatchMultipleRulesWithPeer(boolean matchMultipleRules, boolean partialOnLeft) { + String rules = + "{\n" + + " \"match_multiple_rules\":" + matchMultipleRules + ",\n" + + " \"rules\": [\n" + + " {\n" + + " \"Rule\": {\n" + + " \"name\": \"R1\",\n" + + " \"condition\": {\n" + + " \"AllCondition\": [\n" + + " {\n" + + " \"EqualsExpression\": {\n" + + " \"lhs\": {\n" + + " \"Event\": \"i\"\n" + + " },\n" + + " \"rhs\": {\n" + + " \"Integer\": 0\n" + + " }\n" + + " }\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"actions\": [\n" + + " {\n" + + " \"Action\": {\n" + + " \"action\": \"debug\",\n" + + " \"action_args\": {\n" + + " \"msg\": \"First one matches\"\n" + + " }\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"enabled\": true\n" + + " }\n" + + " },\n" + + " {\n" + + " \"Rule\": {\n" + + " \"name\": \"R2\",\n" + + " \"condition\": {\n" + + " \"AllCondition\": [\n" + + " {\n" + + " \"EqualsExpression\": {\n" + + " \"lhs\": {\n" + + " \"Event\": \" " + (partialOnLeft ? "i" : "j") + " \"\n" + + " },\n" + + " \"rhs\": {\n" + + " \"Integer\": 0\n" + + " }\n" + + " }\n" + + " },\n" + + " {\n" + + " \"EqualsExpression\": {\n" + + " \"lhs\": {\n" + + " \"Event\": \"" + (partialOnLeft ? "j" : "i") + " \"\n" + + " },\n" + + " \"rhs\": {\n" + + " \"Integer\": 0\n" + + " }\n" + + " }\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"actions\": [\n" + + " {\n" + + " \"Action\": {\n" + + " \"action\": \"debug\",\n" + + " \"action_args\": {\n" + + " \"msg\": \"Second one matches\"\n" + + " }\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"enabled\": true\n" + + " }\n" + + " },\n" + + " {\n" + + " \"Rule\": {\n" + + " \"name\": \"R3\",\n" + + " \"condition\": {\n" + + " \"AllCondition\": [\n" + + " {\n" + + " \"EqualsExpression\": {\n" + + " \"lhs\": {\n" + + " \"Event\": \" " + (partialOnLeft ? "i" : "j") + " \"\n" + + " },\n" + + " \"rhs\": {\n" + + " \"Integer\": 0\n" + + " }\n" + + " }\n" + + " },\n" + + " {\n" + + " \"EqualsExpression\": {\n" + + " \"lhs\": {\n" + + " \"Event\": \"" + (partialOnLeft ? "j" : "i") + " \"\n" + + " },\n" + + " \"rhs\": {\n" + + " \"Integer\": 0\n" + + " }\n" + + " }\n" + + " },\n" + + " {\n" + + " \"EqualsExpression\": {\n" + + " \"lhs\": {\n" + + " \"Event\": \"k\"\n" + + " },\n" + + " \"rhs\": {\n" + + " \"Integer\": 0\n" + + " }\n" + + " }\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"actions\": [\n" + + " {\n" + + " \"Action\": {\n" + + " \"action\": \"debug\",\n" + + " \"action_args\": {\n" + + " \"msg\": \"Third one matches\"\n" + + " }\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"enabled\": true\n" + + " }\n" + + " }\n" + + " ]\n" + + " }"; + + RulesExecutor rulesExecutor = RulesExecutorFactory.createFromJson(rules); + + List matchedRules = rulesExecutor.processEvents("{ \"i\" : 0 }").join(); + assertThat(matchedRules).hasSize(1); + assertThat(matchedRules.stream().map(m -> m.getRule().getName())).contains("R1"); + + matchedRules = rulesExecutor.processEvents("{ \"j\" : 0 }").join(); + assertThat(matchedRules).hasSize(matchMultipleRules ? 1 : 0); + + if (matchMultipleRules) { + // when multiple match is allowed i=0 should be retained and now used to also fire R2 + assertThat(matchedRules.stream().map(m -> m.getRule().getName())).contains("R2"); + // i=0 and j=0 have a partial match for R3. Retained + assertThat(rulesExecutor.getAllFacts()).hasSize(2); + } else { + // if R2 never fired j=0 should still be there + assertThat(rulesExecutor.getAllFacts().size()).isEqualTo(1); + } + + matchedRules = rulesExecutor.processEvents("{ \"k\" : 0 }").join(); + assertThat(matchedRules).hasSize(matchMultipleRules ? 1 : 0); + + if (matchMultipleRules) { + // when multiple match is allowed i=0 and j=0 should be retained and now used to also fire R3 + assertThat(matchedRules.stream().map(m -> m.getRule().getName())).contains("R3"); + // if all rules fired now the working memory should be empty + assertThat(rulesExecutor.getAllFacts()).isEmpty(); + } else { + // if R2 and R3 never fired j=0 and k=0 should still be there + assertThat(rulesExecutor.getAllFacts().size()).isEqualTo(2); + } + + rulesExecutor.dispose(); + } + + @Test + public void retainPartialMatchesWithMatchMultipleRulesWithMultiplePeers() { + String rules = + "{\n" + + " \"match_multiple_rules\": true,\n" + + " \"rules\": [\n" + + " {\n" + + " \"Rule\": {\n" + + " \"name\": \"R1\",\n" + + " \"condition\": {\n" + + " \"AllCondition\": [\n" + + " {\n" + + " \"EqualsExpression\": {\n" + + " \"lhs\": {\n" + + " \"Event\": \"i\"\n" + + " },\n" + + " \"rhs\": {\n" + + " \"Integer\": 0\n" + + " }\n" + + " }\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"actions\": [\n" + + " {\n" + + " \"Action\": {\n" + + " \"action\": \"debug\",\n" + + " \"action_args\": {\n" + + " \"msg\": \"First one matches\"\n" + + " }\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"enabled\": true\n" + + " }\n" + + " },\n" + + " {\n" + + " \"Rule\": {\n" + + " \"name\": \"R2\",\n" + + " \"condition\": {\n" + + " \"AllCondition\": [\n" + + " {\n" + + " \"EqualsExpression\": {\n" + + " \"lhs\": {\n" + + " \"Event\": \"i\"\n" + + " },\n" + + " \"rhs\": {\n" + + " \"Integer\": 0\n" + + " }\n" + + " }\n" + + " },\n" + + " {\n" + + " \"EqualsExpression\": {\n" + + " \"lhs\": {\n" + + " \"Event\": \"j\"\n" + + " },\n" + + " \"rhs\": {\n" + + " \"Integer\": 0\n" + + " }\n" + + " }\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"actions\": [\n" + + " {\n" + + " \"Action\": {\n" + + " \"action\": \"debug\",\n" + + " \"action_args\": {\n" + + " \"msg\": \"Second one matches\"\n" + + " }\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"enabled\": true\n" + + " }\n" + + " },\n" + + " {\n" + + " \"Rule\": {\n" + + " \"name\": \"R3\",\n" + + " \"condition\": {\n" + + " \"AllCondition\": [\n" + + " {\n" + + " \"EqualsExpression\": {\n" + + " \"lhs\": {\n" + + " \"Event\": \"i\"\n" + + " },\n" + + " \"rhs\": {\n" + + " \"Integer\": 0\n" + + " }\n" + + " }\n" + + " },\n" + + " {\n" + + " \"EqualsExpression\": {\n" + + " \"lhs\": {\n" + + " \"Event\": \"j\"\n" + + " },\n" + + " \"rhs\": {\n" + + " \"Integer\": 0\n" + + " }\n" + + " }\n" + + " },\n" + + " {\n" + + " \"EqualsExpression\": {\n" + + " \"lhs\": {\n" + + " \"Event\": \"k\"\n" + + " },\n" + + " \"rhs\": {\n" + + " \"Integer\": 0\n" + + " }\n" + + " }\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"actions\": [\n" + + " {\n" + + " \"Action\": {\n" + + " \"action\": \"debug\",\n" + + " \"action_args\": {\n" + + " \"msg\": \"Third one matches\"\n" + + " }\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"enabled\": true\n" + + " }\n" + + " },\n" + + " {\n" + + " \"Rule\": {\n" + + " \"name\": \"R4\",\n" + + " \"condition\": {\n" + + " \"AllCondition\": [\n" + + " {\n" + + " \"EqualsExpression\": {\n" + + " \"lhs\": {\n" + + " \"Event\": \"i\"\n" + + " },\n" + + " \"rhs\": {\n" + + " \"Integer\": 0\n" + + " }\n" + + " }\n" + + " },\n" + + " {\n" + + " \"EqualsExpression\": {\n" + + " \"lhs\": {\n" + + " \"Event\": \"j\"\n" + + " },\n" + + " \"rhs\": {\n" + + " \"Integer\": 0\n" + + " }\n" + + " }\n" + + " },\n" + + " {\n" + + " \"EqualsExpression\": {\n" + + " \"lhs\": {\n" + + " \"Event\": \"l\"\n" + + " },\n" + + " \"rhs\": {\n" + + " \"Integer\": 0\n" + + " }\n" + + " }\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"actions\": [\n" + + " {\n" + + " \"Action\": {\n" + + " \"action\": \"debug\",\n" + + " \"action_args\": {\n" + + " \"msg\": \"Fourth one matches\"\n" + + " }\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"enabled\": true\n" + + " }\n" + + " }\n" + + " ]\n" + + " }"; + + RulesExecutor rulesExecutor = RulesExecutorFactory.createFromJson(rules); + + List matchedRules = rulesExecutor.processEvents("{ \"i\" : 0 }").join(); + assertThat(matchedRules).hasSize(1); + assertThat(matchedRules.stream().map(m -> m.getRule().getName())).contains("R1"); + + matchedRules = rulesExecutor.processEvents("{ \"j\" : 0 }").join(); + assertThat(matchedRules).hasSize(1); + + // when multiple match is allowed i=0 should be retained and now used to also fire R2 + assertThat(matchedRules.stream().map(m -> m.getRule().getName())).contains("R2"); + // i=0 and j=0 have a partial match for R3. Retained + assertThat(rulesExecutor.getAllFacts()).hasSize(2); + + matchedRules = rulesExecutor.processEvents("{ \"k\" : 0 }").join(); + assertThat(matchedRules).hasSize(1); + + // when multiple match is allowed i=0 and j=0 should be retained and now used to also fire R3 + assertThat(matchedRules.stream().map(m -> m.getRule().getName())).contains("R3"); + // i=0 and j=0 have a partial match for R3. Retained. But k=0 is discarded + assertThat(rulesExecutor.getAllFacts()).hasSize(2); + + matchedRules = rulesExecutor.processEvents("{ \"l\" : 0 }").join(); + assertThat(matchedRules).hasSize(1); + + // when multiple match is allowed i=0 and j=0 should be retained and now used to also fire R4 + assertThat(matchedRules.stream().map(m -> m.getRule().getName())).contains("R4"); + // if all rules fired now the working memory should be empty + assertThat(rulesExecutor.getAllFacts()).isEmpty(); + + rulesExecutor.dispose(); + } }