This is an automated email from the ASF dual-hosted git repository.
Rikkola pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-kie.git
The following commit(s) were added to refs/heads/main by this push:
new 9330215b7dc fix(drools): exists(A() and not B()) stops firing when
sibling package wraps same CE in its own exists (#7116)
9330215b7dc is described below
commit 9330215b7dc699e87881f930d24022541cb53609
Author: Toni Rikkola <[email protected]>
AuthorDate: Thu Oct 1 12:36:22 2026 +0300
fix(drools): exists(A() and not B()) stops firing when sibling package
wraps same CE in its own exists (#7116)
When rules from different packages are loaded sequentially, a rule
containing
exists(SharedFact() and not NegatedFact())
silently stops firing if a later package adds a sibling rule containing
exists(SharedFact() and OtherFact())
Root cause (split path): SegmentPrototype.splitEagerProtos bailed out early
when proto1 was not eager, even when the split produced an eager proto2. The
early-exit comment 'if it wasn't eager before, nothing can be eager after'
was
incorrect for the case where proto1 contained both a JoinNode and a NotNode
(suppressing the eager requirement) and the split removed the JoinNode from
proto2's half. Without proto2 in the eager list, initializePathMemory
skipped
creating the NotNode's segment, leaving the ExistsNode permanently unlinked.
Root cause (merge path): the inverse operation, mergeEagerProtos(), had a
symmetric gap. When the sibling rule is later removed and segments merge
back,
the stale eager prototype for proto2 was not cleaned up in the case where
proto1 no longer requiresEager() after the merge. The stale entry caused an
ArrayIndexOutOfBoundsException in
SegmentMemorySupportImpl.initializePathMemory()
when a new session was created after the merge.
Fix: add an else-if branch in splitEagerProtos for the !proto1WasEager &&
proto2.requiresEager() case, and extend the removal condition in
mergeEagerProtos
to also cover the (!requiresEager() && proto2WasEager) case.
Closes: https://github.com/apache/incubator-kie/issues/7115
Tests:
- ExistsWithNotSubnetworkCrossPackageTest (5 methods x 2 configs = 10
instances)
-
SubnetworkSegmentSplitIntegrationTest#testTP04_removeSiblingRule_afterTopLevelNegationSegmentSplit
(2 configs)
Co-authored-by: Toni Rikkola <[email protected]>
Co-authored-by: IBM Bob <[email protected]>
Co-authored-by: Toshiya Kobayashi <[email protected]>
---
.../java/org/drools/core/reteoo/SegmentMemory.java | 20 +-
.../ExistsWithNotSubnetworkCrossPackageTest.java | 466 +++++++++++++++++++++
.../SubnetworkSegmentSplitIntegrationTest.java | 159 +++++++
3 files changed, 640 insertions(+), 5 deletions(-)
diff --git
a/drools-core/src/main/java/org/drools/core/reteoo/SegmentMemory.java
b/drools-core/src/main/java/org/drools/core/reteoo/SegmentMemory.java
index a909328c292..1d7c727dbb6 100644
--- a/drools-core/src/main/java/org/drools/core/reteoo/SegmentMemory.java
+++ b/drools-core/src/main/java/org/drools/core/reteoo/SegmentMemory.java
@@ -853,7 +853,7 @@ public class SegmentMemory extends LinkedList<SegmentMemory>
}
public void splitEagerProtos(boolean proto1WasEager, SegmentPrototype
other, PathEndNode endNode) {
- if (proto1WasEager) { // if it wasn't eager before, nothing can be
eager after
+ if (proto1WasEager) { // proto1 was eager before the split
SegmentPrototype[] eager = endNode.getEagerSegmentPrototypes();
if (requiresEager() && other.requiresEager()) {
// keep proto1 and add proto2
@@ -862,14 +862,24 @@ public class SegmentMemory extends
LinkedList<SegmentMemory>
newEager[newEager.length - 1] = other; // I don't think
order matters, so just add to the end
endNode.setEagerSegmentPrototypes(newEager);
} else if (other.requiresEager()) {
- // proto2 is no longer eager, find proto1 and swap proto1
with proto2
+ // proto1 is no longer eager, find proto1 and swap proto1
with proto2
for (int i = 0; i < eager.length; i++) {
if (eager[i] == this) {
eager[i] = other;
break;
}
}
- } // else if ( requiresEager() && !proto2.requiresEager()) do
nothing as proto1 already in the array
+ }
+ } else if (other.requiresEager()) {
+ // proto1 was not eager, but the split produced an eager
proto2.
+ // This happens when proto1 contained both a NotNode and a
JoinNode (suppressing the
+ // eager requirement), and the split removed the JoinNode from
proto2's portion.
+ // proto2 must be added to the endNode's eager list so it gets
eagerly initialised.
+ SegmentPrototype[] eager = endNode.getEagerSegmentPrototypes();
+ SegmentPrototype[] newEager = new
SegmentPrototype[eager.length + 1];
+ System.arraycopy(eager, 0, newEager, 0, eager.length);
+ newEager[newEager.length - 1] = other;
+ endNode.setEagerSegmentPrototypes(newEager);
}
}
@@ -912,8 +922,8 @@ public class SegmentMemory extends LinkedList<SegmentMemory>
}
SegmentPrototype[] eager = endNode.getEagerSegmentPrototypes();
- if (requiresEager() && proto2.requiresEager()) {
- // keep proto1 and remove proto2
+ if ((!requiresEager() && proto2WasEager) || (requiresEager() &&
proto2.requiresEager())) {
+ // remove proto2 from the eager array
SegmentPrototype[] newEager = new
SegmentPrototype[eager.length - 1];
copyWithRemoval(eager, newEager, proto2);
endNode.setEagerSegmentPrototypes(newEager);
diff --git
a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/ExistsWithNotSubnetworkCrossPackageTest.java
b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/ExistsWithNotSubnetworkCrossPackageTest.java
new file mode 100644
index 00000000000..a04f5863022
--- /dev/null
+++
b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/ExistsWithNotSubnetworkCrossPackageTest.java
@@ -0,0 +1,466 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.drools.compiler.integrationtests;
+
+import java.util.stream.Stream;
+
+import org.drools.testcoverage.common.util.KieBaseTestConfiguration;
+import org.drools.testcoverage.common.util.KieBaseUtil;
+import org.drools.testcoverage.common.util.TestParametersUtil2;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.kie.api.KieBase;
+import org.kie.api.definition.type.FactType;
+import org.kie.api.runtime.KieSession;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ExistsWithNotSubnetworkCrossPackageTest {
+
+ public static Stream<KieBaseTestConfiguration> parameters() {
+ return
TestParametersUtil2.getKieBaseCloudConfigurations(true).stream();
+ }
+
+ private static final String DRL_TYPES =
+ "package repro.types;\n" +
+ "declare Seed end\n" +
+ "declare SharedFact end\n" +
+ "declare NegatedFact end\n" +
+ "declare OtherFact end\n" +
+ "declare Result end\n";
+
+ private static final String DRL_A =
+ "package repro.a;\n" +
+ "import repro.types.*;\n" +
+ "rule \"produce-shared-fact\"\n" +
+ " when\n" +
+ " Seed()\n" +
+ " then\n" +
+ " insert(new SharedFact());\n" +
+ "end\n";
+
+ private static final String DRL_B =
+ "package repro.b;\n" +
+ "import repro.types.*;\n" +
+ "rule \"subject\"\n" +
+ " when\n" +
+ " exists ( SharedFact() and not NegatedFact() )\n" +
+ " then\n" +
+ " insert(new Result());\n" +
+ "end\n";
+
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testExistsWithNotSubnetwork_subjectAloneFiresCorrectly(
+ KieBaseTestConfiguration kieBaseTestConfiguration) throws
Exception {
+
+ final KieBase kbase = KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+ "exists-not-solo-test", kieBaseTestConfiguration,
+ DRL_TYPES, DRL_A, DRL_B);
+
+ final KieSession ksession = kbase.newKieSession();
+ try {
+ final FactType seedType = kbase.getFactType("repro.types", "Seed");
+ ksession.insert(seedType.newInstance());
+ final int fired = ksession.fireAllRules();
+ assertThat(fired)
+ .as("Without sibling, subject must fire (2 rules:
produce-shared-fact + subject)")
+ .isEqualTo(2);
+ } finally {
+ ksession.dispose();
+ }
+ }
+
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testExistsWithNotSubnetworkNotSuppressedByCrossPackageSibling(
+ KieBaseTestConfiguration kieBaseTestConfiguration) throws
Exception {
+
+ final String drlC =
+ "package repro.c;\n" +
+ "import repro.types.*;\n" +
+ "rule \"sibling\"\n" +
+ " when\n" +
+ " exists ( SharedFact() and OtherFact() )\n" +
+ " then\n" +
+ "end\n";
+
+ final KieBase kbase = KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+ "exists-not-cross-package-test", kieBaseTestConfiguration,
+ DRL_TYPES, DRL_A, DRL_B, drlC);
+
+ final KieSession ksession = kbase.newKieSession();
+ try {
+ final FactType seedType = kbase.getFactType("repro.types", "Seed");
+ ksession.insert(seedType.newInstance());
+ final int fired = ksession.fireAllRules();
+
+ assertThat(fired)
+ .as("Expected 2 rule firings (produce-shared-fact +
subject); sibling must not fire")
+ .isEqualTo(2);
+
+ final FactType resultType = kbase.getFactType("repro.types",
"Result");
+ final long resultCount = ksession.getObjects().stream()
+ .filter(o -> resultType.getFactClass().isInstance(o))
+ .count();
+ assertThat(resultCount)
+ .as("subject rule must insert exactly one Result")
+ .isEqualTo(1);
+ } finally {
+ ksession.dispose();
+ }
+ }
+
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testMinimal_directSharedFactInsert(
+ KieBaseTestConfiguration kieBaseTestConfiguration) throws
Exception {
+
+ final String drlB2 =
+ "package repro.b;\n" +
+ "import repro.types.*;\n" +
+ "rule \"subject\"\n" +
+ " when\n" +
+ " exists ( SharedFact() and not NegatedFact() )\n" +
+ " then\n" +
+ " insert(new Result());\n" +
+ "end\n";
+
+ final String drlC2 =
+ "package repro.c;\n" +
+ "import repro.types.*;\n" +
+ "rule \"sibling\"\n" +
+ " when\n" +
+ " exists ( SharedFact() and OtherFact() )\n" +
+ " then\n" +
+ "end\n";
+
+ final KieBase kbaseAlone = KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+ "minimal-alone-test", kieBaseTestConfiguration,
+ DRL_TYPES, drlB2);
+ final KieSession sessAlone = kbaseAlone.newKieSession();
+ try {
+ sessAlone.insert(kbaseAlone.getFactType("repro.types",
"SharedFact").newInstance());
+ final int firedAlone = sessAlone.fireAllRules();
+ assertThat(firedAlone)
+ .as("Without sibling, subject fires directly on SharedFact
insert: expected 1")
+ .isEqualTo(1);
+ } finally {
+ sessAlone.dispose();
+ }
+
+ final KieBase kbaseWith = KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+ "minimal-with-sibling-test", kieBaseTestConfiguration,
+ DRL_TYPES, drlB2, drlC2);
+ final KieSession sessWith = kbaseWith.newKieSession();
+ try {
+ sessWith.insert(kbaseWith.getFactType("repro.types",
"SharedFact").newInstance());
+ final int firedWith = sessWith.fireAllRules();
+ assertThat(firedWith)
+ .as("With sibling (cross-pkg), subject must still fire:
expected 1")
+ .isEqualTo(1);
+ } finally {
+ sessWith.dispose();
+ }
+ }
+
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void
testExistsWithNotSubnetwork_samePkgSiblingDoesNotSuppressSubject(
+ KieBaseTestConfiguration kieBaseTestConfiguration) throws
Exception {
+
+ final String drlCSamePkg =
+ "package repro.b;\n" +
+ "import repro.types.*;\n" +
+ "rule \"sibling-same-pkg\"\n" +
+ " when\n" +
+ " exists ( SharedFact() and OtherFact() )\n" +
+ " then\n" +
+ "end\n";
+
+ final KieBase kbase = KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+ "exists-not-samepkg-test", kieBaseTestConfiguration,
+ DRL_TYPES, DRL_A, DRL_B, drlCSamePkg);
+
+ final KieSession ksession = kbase.newKieSession();
+ try {
+ final FactType seedType = kbase.getFactType("repro.types", "Seed");
+ ksession.insert(seedType.newInstance());
+ final int fired = ksession.fireAllRules();
+ assertThat(fired)
+ .as("Same-pkg sibling must not suppress subject: expected
2 firings (produce-shared-fact + subject)")
+ .isEqualTo(2);
+ } finally {
+ ksession.dispose();
+ }
+ }
+
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testMinimal_javaBeans_crossPackage(
+ KieBaseTestConfiguration kieBaseTestConfiguration) throws
Exception {
+
+ final String drlBJava =
+ "package repro.bj;\n" +
+ "import " + java.util.ArrayList.class.getCanonicalName() +
";\n" +
+ "import " + java.util.LinkedList.class.getCanonicalName() +
";\n" +
+ "import " + java.util.HashMap.class.getCanonicalName() + ";\n"
+
+ "rule \"subjectJ\"\n" +
+ " when\n" +
+ " exists ( ArrayList() and not LinkedList() )\n" +
+ " then\n" +
+ "end\n";
+
+ final String drlCJava =
+ "package repro.cj;\n" +
+ "import " + java.util.ArrayList.class.getCanonicalName() +
";\n" +
+ "import " + java.util.HashMap.class.getCanonicalName() + ";\n"
+
+ "rule \"siblingJ\"\n" +
+ " when\n" +
+ " exists ( ArrayList() and HashMap() )\n" +
+ " then\n" +
+ "end\n";
+
+ final KieBase aloneBase = KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+ "java-alone-test", kieBaseTestConfiguration, drlBJava);
+ final KieSession aloneSession = aloneBase.newKieSession();
+ try {
+ aloneSession.insert(new java.util.ArrayList<>());
+ assertThat(aloneSession.fireAllRules())
+ .as("Java classes, no sibling: subject must fire")
+ .isEqualTo(1);
+ } finally {
+ aloneSession.dispose();
+ }
+
+ final KieBase withBase = KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+ "java-with-sibling-test", kieBaseTestConfiguration, drlBJava,
drlCJava);
+ final KieSession withSession = withBase.newKieSession();
+ try {
+ withSession.insert(new java.util.ArrayList<>());
+ assertThat(withSession.fireAllRules())
+ .as("Java classes, with cross-pkg sibling: subject must
still fire")
+ .isEqualTo(1);
+ } finally {
+ withSession.dispose();
+ }
+ }
+
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testRemoveSiblingRule_subjectStillFires(
+ KieBaseTestConfiguration kieBaseTestConfiguration) throws
Exception {
+
+ final String drlB =
+ "package repro.b;\n" +
+ "import repro.types.*;\n" +
+ "rule \"subject\"\n" +
+ " when\n" +
+ " exists ( SharedFact() and not NegatedFact() )\n" +
+ " then\n" +
+ " insert(new Result());\n" +
+ "end\n";
+
+ final String drlC =
+ "package repro.c;\n" +
+ "import repro.types.*;\n" +
+ "rule \"sibling\"\n" +
+ " when\n" +
+ " exists ( SharedFact() and OtherFact() )\n" +
+ " then\n" +
+ "end\n";
+
+ final KieBase kbase = KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+ "remove-sibling-test", kieBaseTestConfiguration,
+ DRL_TYPES, drlB, drlC);
+
+ kbase.removeRule("repro.c", "sibling");
+
+ final KieSession session = kbase.newKieSession();
+ try {
+ session.insert(kbase.getFactType("repro.types",
"SharedFact").newInstance());
+ final int fired = session.fireAllRules();
+ assertThat(fired)
+ .as("After removing sibling, subject must still fire:
expected 1")
+ .isEqualTo(1);
+ } finally {
+ session.dispose();
+ }
+ }
+
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testNotWithNotSubnetwork_crossPackage(
+ KieBaseTestConfiguration kieBaseTestConfiguration) throws
Exception {
+
+ final String drlB =
+ "package repro.b;\n" +
+ "import repro.types.*;\n" +
+ "rule \"subject-not\"\n" +
+ " when\n" +
+ " not ( SharedFact() and not NegatedFact() )\n" +
+ " then\n" +
+ " insert(new Result());\n" +
+ "end\n";
+
+ final String drlC =
+ "package repro.c;\n" +
+ "import repro.types.*;\n" +
+ "rule \"sibling\"\n" +
+ " when\n" +
+ " exists ( SharedFact() and OtherFact() )\n" +
+ " then\n" +
+ "end\n";
+
+ final KieBase kbase = KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+ "not-with-not-subnetwork-test", kieBaseTestConfiguration,
+ DRL_TYPES, drlB, drlC);
+
+ final KieSession session = kbase.newKieSession();
+ try {
+ final int fired = session.fireAllRules();
+ assertThat(fired)
+ .as("No SharedFact inserted: not(SharedFact and not
NegatedFact) must fire")
+ .isEqualTo(1);
+ } finally {
+ session.dispose();
+ }
+ }
+
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testMultiJoinWithNegation_crossPackage(
+ KieBaseTestConfiguration kieBaseTestConfiguration) throws
Exception {
+
+ final String drlB =
+ "package repro.b;\n" +
+ "import repro.types.*;\n" +
+ "rule \"subject-multijoin\"\n" +
+ " when\n" +
+ " exists ( SharedFact() and OtherFact() and not
NegatedFact() )\n" +
+ " then\n" +
+ " insert(new Result());\n" +
+ "end\n";
+
+ final String drlC =
+ "package repro.c;\n" +
+ "import repro.types.*;\n" +
+ "rule \"sibling-shared-only\"\n" +
+ " when\n" +
+ " exists ( SharedFact() and Seed() )\n" +
+ " then\n" +
+ "end\n";
+
+ final KieBase kbase = KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+ "multijoin-not-test", kieBaseTestConfiguration,
+ DRL_TYPES, drlB, drlC);
+
+ final KieSession session = kbase.newKieSession();
+ try {
+ session.insert(kbase.getFactType("repro.types",
"SharedFact").newInstance());
+ session.insert(kbase.getFactType("repro.types",
"OtherFact").newInstance());
+ final int fired = session.fireAllRules();
+ assertThat(fired)
+ .as("SharedFact + OtherFact inserted: subject-multijoin
must fire")
+ .isEqualTo(1);
+ } finally {
+ session.dispose();
+ }
+ }
+
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testMultipleChainedNegations_crossPackage(
+ KieBaseTestConfiguration kieBaseTestConfiguration) throws
Exception {
+
+ final String drlB =
+ "package repro.b;\n" +
+ "import repro.types.*;\n" +
+ "rule \"subject-double-not\"\n" +
+ " when\n" +
+ " exists ( SharedFact() and not NegatedFact() and not
OtherFact() )\n" +
+ " then\n" +
+ " insert(new Result());\n" +
+ "end\n";
+
+ final String drlC =
+ "package repro.c;\n" +
+ "import repro.types.*;\n" +
+ "rule \"sibling\"\n" +
+ " when\n" +
+ " exists ( SharedFact() and Seed() )\n" +
+ " then\n" +
+ "end\n";
+
+ final KieBase kbase = KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+ "double-not-test", kieBaseTestConfiguration,
+ DRL_TYPES, drlB, drlC);
+
+ final KieSession session = kbase.newKieSession();
+ try {
+ session.insert(kbase.getFactType("repro.types",
"SharedFact").newInstance());
+ final int fired = session.fireAllRules();
+ assertThat(fired)
+ .as("SharedFact inserted without negated facts:
subject-double-not must fire")
+ .isEqualTo(1);
+ } finally {
+ session.dispose();
+ }
+ }
+
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testAccumulateWithNotSubnetwork_crossPackage(
+ KieBaseTestConfiguration kieBaseTestConfiguration) throws
Exception {
+
+ final String drlB =
+ "package repro.b;\n" +
+ "import repro.types.*;\n" +
+ "rule \"subject-accumulate\"\n" +
+ " when\n" +
+ " accumulate( SharedFact() and not NegatedFact(); $c :
count() )\n" +
+ " then\n" +
+ " insert(new Result());\n" +
+ "end\n";
+
+ final String drlC =
+ "package repro.c;\n" +
+ "import repro.types.*;\n" +
+ "rule \"sibling\"\n" +
+ " when\n" +
+ " exists ( SharedFact() and OtherFact() )\n" +
+ " then\n" +
+ "end\n";
+
+ final KieBase kbase = KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+ "accumulate-not-test", kieBaseTestConfiguration,
+ DRL_TYPES, drlB, drlC);
+
+ final KieSession session = kbase.newKieSession();
+ try {
+ session.insert(kbase.getFactType("repro.types",
"SharedFact").newInstance());
+ final int fired = session.fireAllRules();
+ assertThat(fired)
+ .as("SharedFact inserted: accumulate should count 1 and
fire")
+ .isEqualTo(1);
+ } finally {
+ session.dispose();
+ }
+ }
+}
diff --git
a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/SubnetworkSegmentSplitIntegrationTest.java
b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/SubnetworkSegmentSplitIntegrationTest.java
new file mode 100644
index 00000000000..817cbcfc42f
--- /dev/null
+++
b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/SubnetworkSegmentSplitIntegrationTest.java
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.drools.compiler.integrationtests;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.drools.testcoverage.common.util.KieBaseTestConfiguration;
+import org.drools.testcoverage.common.util.KieBaseUtil;
+import org.drools.testcoverage.common.util.TestParametersUtil2;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.drools.kiesession.rulebase.InternalKnowledgeBase;
+import org.kie.api.KieBase;
+import org.kie.api.runtime.KieSession;
+import org.kie.api.runtime.rule.FactHandle;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SubnetworkSegmentSplitIntegrationTest {
+
+ public static Stream<KieBaseTestConfiguration> parameters() {
+ return
TestParametersUtil2.getKieBaseCloudConfigurations(true).stream();
+ }
+
+ public static class Container {
+ private final List<String> items;
+ public Container(String... items) { this.items = new
ArrayList<>(Arrays.asList(items)); }
+ public List<String> getItems() { return items; }
+ }
+
+ // =======================================================================
+ // TP-01: Mixed exists + not sharing on cross-package subnetwork
+ // =======================================================================
+ private static final String DRL_TP01_PKG_A =
+ "package repro.tp01.a;\n" +
+ "import " + Container.class.getCanonicalName() + ";\n" +
+ "global java.util.List results;\n" +
+ "rule \"not-rule\"\n" +
+ "when\n" +
+ " not ( $c : Container() and String( this == \"match\" ) from
$c.items )\n" +
+ "then\n" +
+ " results.add(\"not-rule\");\n" +
+ "end\n";
+
+ private static final String DRL_TP01_PKG_B =
+ "package repro.tp01.b;\n" +
+ "import " + Container.class.getCanonicalName() + ";\n" +
+ "global java.util.List results;\n" +
+ "rule \"exists-rule\"\n" +
+ "when\n" +
+ " exists ( $c : Container() and String( this == \"match\" )
from $c.items )\n" +
+ "then\n" +
+ " results.add(\"exists-rule\");\n" +
+ "end\n";
+
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void
testTP01_mixedExistsAndNot_crossPackageSharing(KieBaseTestConfiguration cfg) {
+ KieBase kbase = KieBaseUtil.getKieBaseFromKieModuleFromDrl("tp01",
cfg, DRL_TP01_PKG_A, DRL_TP01_PKG_B);
+ KieSession ks = kbase.newKieSession();
+ try {
+ List<String> results = new ArrayList<>();
+ ks.setGlobal("results", results);
+
+ // Initial state: no Container inserted.
+ // not-rule must fire (no matching items), exists-rule must not
fire.
+ ks.fireAllRules();
+ assertThat(results).as("Empty session: not-rule fires, exists does
not").containsExactly("not-rule");
+ results.clear();
+
+ // Insert Container with matching item
+ FactHandle fh = ks.insert(new Container("match"));
+ ks.fireAllRules();
+ assertThat(results).as("With matching container: exists-rule
fires, not does not").containsExactly("exists-rule");
+ results.clear();
+
+ // Retract Container
+ ks.delete(fh);
+ ks.fireAllRules();
+ assertThat(results).as("After retraction: not-rule fires again,
exists does not").containsExactly("not-rule");
+ } finally {
+ ks.dispose();
+ }
+ }
+
+ // =======================================================================
+ // TP-04: removeRule cleans up stale eager prototype after segment merge
+ // (Toshiya's comment on PR #7116)
+ // =======================================================================
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testTP04_removeSiblingRule_afterTopLevelNegationSegmentSplit(
+ KieBaseTestConfiguration kieBaseTestConfiguration) {
+
+ final String drlSubject =
+ "package repro.b;\n" +
+ "import java.util.ArrayList;\n" +
+ "import java.util.LinkedList;\n" +
+ "rule \"subject\"\n" +
+ " when\n" +
+ " String()\n" +
+ " ArrayList()\n" +
+ " not LinkedList()\n" +
+ " then\n" +
+ "end\n";
+
+ final String drlSibling =
+ "package repro.c;\n" +
+ "import java.util.ArrayList;\n" +
+ "import java.util.HashMap;\n" +
+ "rule \"sibling\"\n" +
+ " when\n" +
+ " String()\n" +
+ " ArrayList()\n" +
+ " HashMap()\n" +
+ " then\n" +
+ "end\n";
+
+ final InternalKnowledgeBase kbase = (InternalKnowledgeBase)
KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+ "remove-sibling-top-level-not-test", kieBaseTestConfiguration,
drlSubject);
+ final KieBase siblingBase = KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+ "remove-sibling-top-level-not-sibling",
kieBaseTestConfiguration, drlSibling);
+
+ // Add the sibling after the subject's segment exists, forcing a split
that makes the NotNode segment eager.
+ kbase.addPackages(siblingBase.getKiePackages());
+ // Merging the segments again must remove the obsolete eager prototype
before a new session is created.
+ kbase.removeRule("repro.c", "sibling");
+
+ final KieSession session = kbase.newKieSession();
+ try {
+ session.insert("start");
+ session.insert(new ArrayList<>());
+ assertThat(session.fireAllRules())
+ .as("After removing sibling and merging segments, subject
must still fire")
+ .isEqualTo(1);
+ } finally {
+ session.dispose();
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]