This is an automated email from the ASF dual-hosted git repository.
tkobayas 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 58873e5f6e8 [incubator-kie#7221] Add test coverage for
insert/delete/update in se… (#7223)
58873e5f6e8 is described below
commit 58873e5f6e83b1bac7db1a3cd4fa7e1834406520
Author: Toshiya Kobayashi <[email protected]>
AuthorDate: Mon Oct 26 15:56:47 2026 +0900
[incubator-kie#7221] Add test coverage for insert/delete/update in se…
(#7223)
* [incubator-kie#7221] Add test coverage for insert/delete/update in
sequential mode
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* use agenda-group
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
---
.../SequentialInsertDeleteUpdateTest.java | 1055 ++++++++++++++++++++
1 file changed, 1055 insertions(+)
diff --git
a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/sequential/SequentialInsertDeleteUpdateTest.java
b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/sequential/SequentialInsertDeleteUpdateTest.java
new file mode 100644
index 00000000000..e07cb13599d
--- /dev/null
+++
b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/sequential/SequentialInsertDeleteUpdateTest.java
@@ -0,0 +1,1055 @@
+/*
+ * 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.mvel.integrationtests.sequential;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.drools.mvel.compiler.Cheese;
+import org.drools.mvel.compiler.Person;
+import org.drools.testcoverage.common.util.KieBaseTestConfiguration;
+import org.drools.testcoverage.common.util.KieBaseUtil;
+import org.drools.testcoverage.common.util.KieUtil;
+import org.drools.testcoverage.common.util.TestParametersUtil2;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.kie.api.KieBase;
+import org.kie.api.builder.KieModule;
+import org.kie.api.conf.SequentialOption;
+import org.kie.api.runtime.StatelessKieSession;
+import org.kie.internal.command.CommandFactory;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests how insert, delete and update in the RHS work in sequential mode.
+ * <p>
+ * In sequential mode, rules are evaluated one by one in salience and load
order, and each rule is evaluated only once.
+ * A rule which has not been evaluated yet sees the working memory changes
made by previously fired rules.
+ * A rule which has already been evaluated is not re-evaluated.
+ *
+ * @see <a
href="https://github.com/apache/incubator-kie/issues/7221">incubator-kie#7221</a>
+ */
+public class SequentialInsertDeleteUpdateTest {
+
+ public static Stream<KieBaseTestConfiguration> parameters() {
+ return
TestParametersUtil2.getKieBaseCloudConfigurations(true).stream();
+ }
+
+ // =====================================================================
+ // insert tests
+ // =====================================================================
+
+ /**
+ * R2 has not been evaluated yet, so it matches the Cheese inserted by R1.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testInsertMatchedByLaterRule(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 salience 100 when\n" +
+ " $p : Person()\n" +
+ "then\n" +
+ " insert( new Cheese( \"stilton\", 10 ) );\n" +
+ " list.add( \"R1\" );\n" +
+ "end\n" +
+ "rule R2 when\n" +
+ " $c : Cheese()\n" +
+ "then\n" +
+ " list.add( \"R2:\" + $c.getType() );\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
new Person("p1", "stilton", 0));
+ assertThat(list).containsExactly("R1", "R2:stilton");
+ }
+
+ /**
+ * R1 has already been evaluated when R2 inserts a Cheese, so R1 is not
re-evaluated.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testInsertNotMatchedByEarlierRule(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 salience 100 when\n" +
+ " $c : Cheese()\n" +
+ "then\n" +
+ " list.add( \"R1:\" + $c.getType() );\n" +
+ "end\n" +
+ "rule R2 when\n" +
+ " $p : Person()\n" +
+ "then\n" +
+ " insert( new Cheese( \"stilton\", 10 ) );\n" +
+ " list.add( \"R2\" );\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
new Person("p1", "stilton", 0));
+ assertThat(list).containsExactly("R2");
+ }
+
+ /**
+ * R1 inserts a Person which matches R1 itself. R1 is not re-evaluated, so
it doesn't loop even without no-loop.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testInsertNotMatchedBySameRule(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 when\n" +
+ " $p : Person()\n" +
+ "then\n" +
+ " insert( new Person( $p.getName() + \"_child\",
\"stilton\", 0 ) );\n" +
+ " list.add( \"R1:\" + $p.getName() );\n" +
+ "end\n" +
+ "rule R2 when\n" +
+ " $p : Person()\n" +
+ "then\n" +
+ " list.add( \"R2:\" + $p.getName() );\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
new Person("p1", "stilton", 0));
+ assertThat(list).containsExactlyInAnyOrder("R1:p1", "R2:p1",
"R2:p1_child");
+ assertThat(list.get(0)).isEqualTo("R1:p1");
+ }
+
+ /**
+ * Inserting an object which is already in the working memory.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ @Disabled("Behaves differently from non-sequential mode. See
https://github.com/apache/incubator-kie/issues/7222")
+ public void testInsertSameObjectAgain(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 salience 100 when\n" +
+ " $c : Cheese()\n" +
+ "then\n" +
+ " insert( $c );\n" +
+ " list.add( \"R1\" );\n" +
+ "end\n" +
+ "rule R2 when\n" +
+ " $c : Cheese()\n" +
+ "then\n" +
+ " list.add( \"R2:\" + $c.getType() );\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
new Cheese("stilton", 10));
+ assertThat(list).containsExactly("R1", "R2:stilton");
+ }
+
+ /**
+ * JoinNode - right insert: R1 inserts a Cheese which joins with the
existing Person in R2.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testInsertJoinRight(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 salience 100 when\n" +
+ " $p : Person()\n" +
+ "then\n" +
+ " insert( new Cheese( $p.getLikes(), 10 ) );\n" +
+ " list.add( \"R1\" );\n" +
+ "end\n" +
+ "rule R2 when\n" +
+ " $p : Person()\n" +
+ " $c : Cheese( type == $p.likes )\n" +
+ "then\n" +
+ " list.add( \"R2:\" + $p.getName() + \":\" + $c.getType()
);\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
new Person("p1", "stilton", 0));
+ assertThat(list).containsExactly("R1", "R2:p1:stilton");
+ }
+
+ /**
+ * JoinNode - left insert: R1 inserts a Person which joins with the
existing Cheese in R2.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testInsertJoinLeft(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 salience 100 when\n" +
+ " $c : Cheese()\n" +
+ "then\n" +
+ " insert( new Person( \"p1\", $c.getType(), 0 ) );\n" +
+ " list.add( \"R1\" );\n" +
+ "end\n" +
+ "rule R2 when\n" +
+ " $p : Person()\n" +
+ " $c : Cheese( type == $p.likes )\n" +
+ "then\n" +
+ " list.add( \"R2:\" + $p.getName() + \":\" + $c.getType()
);\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
new Cheese("stilton", 10));
+ assertThat(list).containsExactly("R1", "R2:p1:stilton");
+ }
+
+ /**
+ * NotNode - right insert: R1 inserts a cheddar Cheese, which blocks R2's
"not Cheese( type == 'cheddar' )".
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testInsertWithNot(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 salience 100 when\n" +
+ " $p : Person()\n" +
+ "then\n" +
+ " insert( new Cheese( \"cheddar\", 10 ) );\n" +
+ " list.add( \"R1\" );\n" +
+ "end\n" +
+ "rule R2 when\n" +
+ " $p : Person()\n" +
+ " not Cheese( type == \"cheddar\" )\n" +
+ "then\n" +
+ " list.add( \"R2\" );\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
new Person("p1", "stilton", 0));
+ assertThat(list).containsExactly("R1");
+ }
+
+ /**
+ * ExistsNode - right insert: R1 inserts a stilton Cheese, which satisfies
R2's "exists Cheese( type == 'stilton' )".
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testInsertWithExists(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 salience 100 when\n" +
+ " $p : Person()\n" +
+ "then\n" +
+ " insert( new Cheese( \"stilton\", 10 ) );\n" +
+ " list.add( \"R1\" );\n" +
+ "end\n" +
+ "rule R2 when\n" +
+ " $p : Person()\n" +
+ " exists Cheese( type == \"stilton\" )\n" +
+ "then\n" +
+ " list.add( \"R2\" );\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
new Person("p1", "stilton", 0));
+ assertThat(list).containsExactly("R1", "R2");
+ }
+
+ /**
+ * AccumulateNode - right insert: R1 inserts a Cheese, which is included
in R2's accumulate.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testInsertWithAccumulate(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 salience 100 when\n" +
+ " $p : Person()\n" +
+ "then\n" +
+ " insert( new Cheese( \"stilton\", 20 ) );\n" +
+ " list.add( \"R1\" );\n" +
+ "end\n" +
+ "rule R2 when\n" +
+ " $p : Person()\n" +
+ " $total : Number() from accumulate(\n" +
+ " Cheese( type == $p.likes, $price : price ),\n" +
+ " sum( $price )\n" +
+ " )\n" +
+ "then\n" +
+ " list.add( \"R2:\" + $total.intValue() );\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
new Person("p1", "stilton", 0), new Cheese("stilton", 10));
+ assertThat(list).containsExactly("R1", "R2:30");
+ }
+
+ // =====================================================================
+ // delete tests
+ // =====================================================================
+
+ /**
+ * R2 has not been evaluated yet, so it doesn't match the Cheese deleted
by R1.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testDeleteNotMatchedByLaterRule(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 salience 100 when\n" +
+ " $c : Cheese( type == \"stilton\" )\n" +
+ "then\n" +
+ " delete( $c );\n" +
+ " list.add( \"R1\" );\n" +
+ "end\n" +
+ "rule R2 when\n" +
+ " $c : Cheese()\n" +
+ "then\n" +
+ " list.add( \"R2:\" + $c.getType() );\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
new Cheese("stilton", 10), new Cheese("cheddar", 10));
+ assertThat(list).containsExactly("R1", "R2:cheddar");
+ }
+
+ /**
+ * R1 has already fired for the Cheese when R2 deletes it. The deletion
doesn't affect R1.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testDeleteAfterEarlierRuleFired(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 salience 100 when\n" +
+ " $c : Cheese()\n" +
+ "then\n" +
+ " list.add( \"R1:\" + $c.getType() );\n" +
+ "end\n" +
+ "rule R2 salience 50 when\n" +
+ " $c : Cheese()\n" +
+ "then\n" +
+ " delete( $c );\n" +
+ " list.add( \"R2:\" + $c.getType() );\n" +
+ "end\n" +
+ "rule R3 when\n" +
+ " $c : Cheese()\n" +
+ "then\n" +
+ " list.add( \"R3:\" + $c.getType() );\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
new Cheese("stilton", 10));
+ assertThat(list).containsExactly("R1:stilton", "R2:stilton");
+ }
+
+ /**
+ * A single-use Coupon is deleted when it is applied. R1 has 2 matches
(one per Order) with the same Coupon.
+ * The first firing deletes the Coupon, so the other match is cancelled as
it is in non-sequential mode.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ @Disabled("Behaves differently from non-sequential mode. See
https://github.com/apache/incubator-kie/issues/7222")
+ public void
testDeleteFactOfPendingMatchOfSameRule(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Order.class.getCanonicalName() + "\n" +
+ "import " + Coupon.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 when\n" +
+ " $o : Order( discounted == false )\n" +
+ " $c : Coupon()\n" +
+ "then\n" +
+ " modify( $o ) { setTotal( $o.getTotal() - $c.getDiscount()
), setDiscounted( true ) };\n" +
+ " delete( $c );\n" +
+ " list.add( \"R1:\" + $o.getId() + \":\" + $c.getCode()
);\n" +
+ "end\n";
+
+ Order o1 = new Order("o1", 100);
+ Order o2 = new Order("o2", 100);
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
o1, o2, new Coupon("A", 10));
+ assertThat(list).hasSize(1);
+ assertThat(Stream.of(o1, o2).filter(Order::isDiscounted)).hasSize(1);
+ }
+
+ /**
+ * JoinNode - right delete: R1 deletes the Cheese which is part of R2's
match.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testDeleteJoinRight(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 salience 100 when\n" +
+ " $c : Cheese( type == \"stilton\" )\n" +
+ "then\n" +
+ " delete( $c );\n" +
+ " list.add( \"R1\" );\n" +
+ "end\n" +
+ "rule R2 when\n" +
+ " $p : Person()\n" +
+ " $c : Cheese( type == $p.likes )\n" +
+ "then\n" +
+ " list.add( \"R2:\" + $p.getName() + \":\" + $c.getType()
);\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
+ new Person("p1", "stilton", 0),
new Person("p2", "cheddar", 0),
+ new Cheese("stilton", 10), new
Cheese("cheddar", 10));
+ assertThat(list).containsExactly("R1", "R2:p2:cheddar");
+ }
+
+ /**
+ * JoinNode - left delete: R1 deletes the Person which is part of R2's
match.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testDeleteJoinLeft(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 salience 100 when\n" +
+ " $p : Person( name == \"p1\" )\n" +
+ "then\n" +
+ " delete( $p );\n" +
+ " list.add( \"R1\" );\n" +
+ "end\n" +
+ "rule R2 when\n" +
+ " $p : Person()\n" +
+ " $c : Cheese( type == $p.likes )\n" +
+ "then\n" +
+ " list.add( \"R2:\" + $p.getName() + \":\" + $c.getType()
);\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
+ new Person("p1", "stilton", 0),
new Person("p2", "cheddar", 0),
+ new Cheese("stilton", 10), new
Cheese("cheddar", 10));
+ assertThat(list).containsExactly("R1", "R2:p2:cheddar");
+ }
+
+ /**
+ * NotNode - right delete: R1 deletes the cheddar Cheese, which unblocks
R2's "not Cheese( type == 'cheddar' )".
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testDeleteWithNot(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 salience 100 when\n" +
+ " $c : Cheese( type == \"cheddar\" )\n" +
+ "then\n" +
+ " delete( $c );\n" +
+ " list.add( \"R1\" );\n" +
+ "end\n" +
+ "rule R2 when\n" +
+ " $p : Person()\n" +
+ " not Cheese( type == \"cheddar\" )\n" +
+ "then\n" +
+ " list.add( \"R2\" );\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
new Person("p1", "stilton", 0), new Cheese("cheddar", 10));
+ assertThat(list).containsExactly("R1", "R2");
+ }
+
+ /**
+ * ExistsNode - right delete: R1 deletes the only stilton Cheese, so R2's
"exists Cheese( type == 'stilton' )" fails.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testDeleteWithExists(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 salience 100 when\n" +
+ " $c : Cheese( type == \"stilton\" )\n" +
+ "then\n" +
+ " delete( $c );\n" +
+ " list.add( \"R1\" );\n" +
+ "end\n" +
+ "rule R2 when\n" +
+ " $p : Person()\n" +
+ " exists Cheese( type == \"stilton\" )\n" +
+ "then\n" +
+ " list.add( \"R2\" );\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
new Person("p1", "stilton", 0), new Cheese("stilton", 10));
+ assertThat(list).containsExactly("R1");
+ }
+
+ /**
+ * AccumulateNode - right delete: R1 deletes a Cheese, which is excluded
from R2's accumulate.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testDeleteWithAccumulate(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 salience 100 when\n" +
+ " $c : Cheese( price == 20 )\n" +
+ "then\n" +
+ " delete( $c );\n" +
+ " list.add( \"R1\" );\n" +
+ "end\n" +
+ "rule R2 when\n" +
+ " $p : Person()\n" +
+ " $total : Number() from accumulate(\n" +
+ " Cheese( type == $p.likes, $price : price ),\n" +
+ " sum( $price )\n" +
+ " )\n" +
+ "then\n" +
+ " list.add( \"R2:\" + $total.intValue() );\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
+ new Person("p1", "stilton", 0),
new Cheese("stilton", 10), new Cheese("stilton", 20));
+ assertThat(list).containsExactly("R1", "R2:10");
+ }
+
+ /**
+ * R1 inserts a Cheese and R2 deletes it. R3 doesn't match the Cheese.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testDeleteInsertedFact(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 salience 100 when\n" +
+ " $p : Person()\n" +
+ "then\n" +
+ " insert( new Cheese( \"stilton\", 10 ) );\n" +
+ " list.add( \"R1\" );\n" +
+ "end\n" +
+ "rule R2 salience 50 when\n" +
+ " $c : Cheese()\n" +
+ "then\n" +
+ " delete( $c );\n" +
+ " list.add( \"R2:\" + $c.getType() );\n" +
+ "end\n" +
+ "rule R3 when\n" +
+ " $p : Person()\n" +
+ " $c : Cheese()\n" +
+ "then\n" +
+ " list.add( \"R3:\" + $c.getType() );\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
new Person("p1", "stilton", 0));
+ assertThat(list).containsExactly("R1", "R2:stilton");
+ }
+
+ // =====================================================================
+ // update tests
+ // =====================================================================
+
+ /**
+ * R1 changes the age with a setter and calls update(). R2 matches the new
value and R3 doesn't match the old value.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testUpdateMatchedByLaterRule(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 salience 100 when\n" +
+ " $p : Person( age == 0 )\n" +
+ "then\n" +
+ " $p.setAge( 1 );\n" +
+ " update( $p );\n" +
+ " list.add( \"R1\" );\n" +
+ "end\n" +
+ "rule R2 salience 50 when\n" +
+ " $p : Person( age == 1 )\n" +
+ "then\n" +
+ " list.add( \"R2\" );\n" +
+ "end\n" +
+ "rule R3 when\n" +
+ " $p : Person( age == 0 )\n" +
+ "then\n" +
+ " list.add( \"R3\" );\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
new Person("p1", "stilton", 0));
+ assertThat(list).containsExactly("R1", "R2");
+ }
+
+ /**
+ * R1 has already been evaluated when R2 updates the Person, so R1 is not
re-evaluated.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testUpdateNotMatchedByEarlierRule(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 salience 100 when\n" +
+ " $p : Person( age == 1 )\n" +
+ "then\n" +
+ " list.add( \"R1\" );\n" +
+ "end\n" +
+ "rule R2 when\n" +
+ " $p : Person( age == 0 )\n" +
+ "then\n" +
+ " $p.setAge( 1 );\n" +
+ " update( $p );\n" +
+ " list.add( \"R2\" );\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
new Person("p1", "stilton", 0));
+ assertThat(list).containsExactly("R2");
+ }
+
+ /**
+ * R1 updates the Person, which still matches R1 itself. R1 is not
re-evaluated, so it doesn't loop even without no-loop.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testUpdateNotMatchedBySameRule(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 when\n" +
+ " $p : Person( age >= 0 )\n" +
+ "then\n" +
+ " $p.setAge( $p.getAge() + 1 );\n" +
+ " update( $p );\n" +
+ " list.add( \"R1:\" + $p.getAge() );\n" +
+ "end\n" +
+ "rule R2 when\n" +
+ " $p : Person()\n" +
+ "then\n" +
+ " list.add( \"R2:\" + $p.getAge() );\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
new Person("p1", "stilton", 0));
+ assertThat(list).containsExactly("R1:1", "R2:1");
+ }
+
+ /**
+ * Only one Coupon can be applied to an Order. R1 has 2 matches (one per
Coupon) with the same Order.
+ * The first firing updates the Order to discounted, so the other match is
cancelled as it is in non-sequential mode.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ @Disabled("Behaves differently from non-sequential mode. See
https://github.com/apache/incubator-kie/issues/7222")
+ public void
testUpdateFactOfPendingMatchOfSameRule(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Order.class.getCanonicalName() + "\n" +
+ "import " + Coupon.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 when\n" +
+ " $o : Order( discounted == false )\n" +
+ " $c : Coupon()\n" +
+ "then\n" +
+ " modify( $o ) { setTotal( $o.getTotal() - $c.getDiscount()
), setDiscounted( true ) };\n" +
+ " list.add( \"R1:\" + $o.getId() + \":\" + $c.getCode()
);\n" +
+ "end\n";
+
+ Order o1 = new Order("o1", 100);
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
o1, new Coupon("A", 10), new Coupon("B", 20));
+ assertThat(list).hasSize(1);
+ assertThat(o1.getTotal()).isIn(90, 80);
+ }
+
+ /**
+ * JoinNode - right update: R1 updates the Cheese type, so R2 joins the
Person with the updated Cheese.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testUpdateJoin(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 salience 100 when\n" +
+ " $c : Cheese( type == \"stilton\" )\n" +
+ "then\n" +
+ " $c.setType( \"cheddar\" );\n" +
+ " update( $c );\n" +
+ " list.add( \"R1\" );\n" +
+ "end\n" +
+ "rule R2 when\n" +
+ " $p : Person()\n" +
+ " $c : Cheese( type == $p.likes )\n" +
+ "then\n" +
+ " list.add( \"R2:\" + $p.getName() + \":\" + $c.getType()
);\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
+ new Person("p1", "stilton", 0),
new Person("p2", "cheddar", 0), new Cheese("stilton", 10));
+ assertThat(list).containsExactly("R1", "R2:p2:cheddar");
+ }
+
+ /**
+ * NotNode / ExistsNode - right update: R1 updates the Cheese from stilton
to cheddar.
+ * R2's "not Cheese( type == 'stilton' )" is unblocked and R3's "exists
Cheese( type == 'stilton' )" fails.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testUpdateWithNotAndExists(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 salience 100 when\n" +
+ " $c : Cheese( type == \"stilton\" )\n" +
+ "then\n" +
+ " $c.setType( \"cheddar\" );\n" +
+ " update( $c );\n" +
+ " list.add( \"R1\" );\n" +
+ "end\n" +
+ "rule R2 salience 50 when\n" +
+ " $p : Person()\n" +
+ " not Cheese( type == \"stilton\" )\n" +
+ "then\n" +
+ " list.add( \"R2\" );\n" +
+ "end\n" +
+ "rule R3 when\n" +
+ " $p : Person()\n" +
+ " exists Cheese( type == \"stilton\" )\n" +
+ "then\n" +
+ " list.add( \"R3\" );\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
new Person("p1", "stilton", 0), new Cheese("stilton", 10));
+ assertThat(list).containsExactly("R1", "R2");
+ }
+
+ /**
+ * AccumulateNode - right update: R1 updates the Cheese price, so R2's
accumulate uses the updated price.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testUpdateWithAccumulate(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 salience 100 when\n" +
+ " $c : Cheese( price == 20 )\n" +
+ "then\n" +
+ " $c.setPrice( 50 );\n" +
+ " update( $c );\n" +
+ " list.add( \"R1\" );\n" +
+ "end\n" +
+ "rule R2 when\n" +
+ " $p : Person()\n" +
+ " $total : Number() from accumulate(\n" +
+ " Cheese( type == $p.likes, $price : price ),\n" +
+ " sum( $price )\n" +
+ " )\n" +
+ "then\n" +
+ " list.add( \"R2:\" + $total.intValue() );\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
+ new Person("p1", "stilton", 0),
new Cheese("stilton", 10), new Cheese("stilton", 20));
+ assertThat(list).containsExactly("R1", "R2:60");
+ }
+
+ // =====================================================================
+ // insert / delete / update combined tests
+ // =====================================================================
+
+ /**
+ * R1 inserts, R2 updates and R3 deletes. Each rule sees the changes made
by the previously fired rules.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testInsertUpdateDeleteChain(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 salience 100 when\n" +
+ " $p : Person()\n" +
+ "then\n" +
+ " insert( new Cheese( \"stilton\", 10 ) );\n" +
+ " list.add( \"R1\" );\n" +
+ "end\n" +
+ "rule R2 salience 90 when\n" +
+ " $c : Cheese( price == 10 )\n" +
+ "then\n" +
+ " modify( $c ) { setPrice( 20 ) };\n" +
+ " list.add( \"R2\" );\n" +
+ "end\n" +
+ "rule R3 salience 80 when\n" +
+ " $p : Person()\n" +
+ " $c : Cheese( type == $p.likes, price == 20 )\n" +
+ "then\n" +
+ " list.add( \"R3:\" + $c.getPrice() );\n" +
+ "end\n" +
+ "rule R4 salience 70 when\n" +
+ " $c : Cheese()\n" +
+ "then\n" +
+ " delete( $c );\n" +
+ " list.add( \"R4\" );\n" +
+ "end\n" +
+ "rule R5 salience 60 when\n" +
+ " $p : Person()\n" +
+ " not Cheese()\n" +
+ "then\n" +
+ " list.add( \"R5\" );\n" +
+ "end\n" +
+ "rule R6 when\n" +
+ " $c : Cheese()\n" +
+ "then\n" +
+ " list.add( \"R6\" );\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
new Person("p1", "stilton", 0));
+ assertThat(list).containsExactly("R1", "R2", "R3:20", "R4", "R5");
+ }
+
+ /**
+ * Without salience, rules are evaluated in load order. R2 sees the Cheese
inserted by R1,
+ * but R1 doesn't see the Person inserted by R2.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testInsertWithLoadOrder(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 when\n" +
+ " $p : Person()\n" +
+ "then\n" +
+ " insert( new Cheese( \"stilton\", 10 ) );\n" +
+ " list.add( \"R1:\" + $p.getName() );\n" +
+ "end\n" +
+ "rule R2 when\n" +
+ " $c : Cheese()\n" +
+ "then\n" +
+ " insert( new Person( \"p2\", \"stilton\", 0 ) );\n" +
+ " list.add( \"R2:\" + $c.getType() );\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
new Person("p1", "stilton", 0));
+ assertThat(list).containsExactly("R1:p1", "R2:stilton");
+ }
+
+ // =====================================================================
+ // agenda-group tests
+ // =====================================================================
+
+ /**
+ * g1 has the focus first. A rule in g1 inserts a Cheese, which is matched
by a rule in g2 evaluated later.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void testInsertMatchedByLaterAgendaGroup(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R0 when\n" +
+ "then\n" +
+ " drools.getKnowledgeRuntime().getAgenda().getAgendaGroup(
\"g2\" ).setFocus();\n" +
+ " drools.getKnowledgeRuntime().getAgenda().getAgendaGroup(
\"g1\" ).setFocus();\n" +
+ "end\n" +
+ "rule R1 agenda-group \"g1\" when\n" +
+ " $p : Person()\n" +
+ "then\n" +
+ " insert( new Cheese( \"stilton\", 10 ) );\n" +
+ " list.add( \"R1\" );\n" +
+ "end\n" +
+ "rule R2 agenda-group \"g2\" when\n" +
+ " $c : Cheese()\n" +
+ "then\n" +
+ " list.add( \"R2:\" + $c.getType() );\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
new Person("p1", "stilton", 0));
+ assertThat(list).containsExactly("R1", "R2:stilton");
+ }
+
+ /**
+ * g2 has the focus first. A rule in g1 inserts a Cheese, but g2 has
already been visited, so the rule in g2 is not re-evaluated.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void
testInsertNotMatchedByVisitedAgendaGroup(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R0 when\n" +
+ "then\n" +
+ " drools.getKnowledgeRuntime().getAgenda().getAgendaGroup(
\"g1\" ).setFocus();\n" +
+ " drools.getKnowledgeRuntime().getAgenda().getAgendaGroup(
\"g2\" ).setFocus();\n" +
+ "end\n" +
+ "rule R1 agenda-group \"g1\" when\n" +
+ " $p : Person()\n" +
+ "then\n" +
+ " insert( new Cheese( \"stilton\", 10 ) );\n" +
+ " list.add( \"R1\" );\n" +
+ "end\n" +
+ "rule R2 agenda-group \"g2\" when\n" +
+ " $c : Cheese()\n" +
+ "then\n" +
+ " list.add( \"R2:\" + $c.getType() );\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
new Person("p1", "stilton", 0));
+ assertThat(list).containsExactly("R1");
+ }
+
+ /**
+ * g1 has the focus first. A rule in g1 deletes a Cheese, so a rule in g2
evaluated later doesn't match it.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void
testDeleteNotMatchedByLaterAgendaGroup(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R0 when\n" +
+ "then\n" +
+ " drools.getKnowledgeRuntime().getAgenda().getAgendaGroup(
\"g2\" ).setFocus();\n" +
+ " drools.getKnowledgeRuntime().getAgenda().getAgendaGroup(
\"g1\" ).setFocus();\n" +
+ "end\n" +
+ "rule R1 agenda-group \"g1\" when\n" +
+ " $c : Cheese( type == \"stilton\" )\n" +
+ "then\n" +
+ " delete( $c );\n" +
+ " list.add( \"R1\" );\n" +
+ "end\n" +
+ "rule R2 agenda-group \"g2\" when\n" +
+ " $c : Cheese()\n" +
+ "then\n" +
+ " list.add( \"R2:\" + $c.getType() );\n" +
+ "end\n";
+
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
new Cheese("stilton", 10), new Cheese("cheddar", 10));
+ assertThat(list).containsExactly("R1", "R2:cheddar");
+ }
+
+ // =====================================================================
+ // Helper methods
+ // =====================================================================
+
+ private List<String> executeSequential(String drl,
KieBaseTestConfiguration kieBaseTestConfiguration, Object... facts) {
+ final KieModule kieModule = KieUtil.getKieModuleFromDrls("test",
kieBaseTestConfiguration, drl);
+ final KieBase kbase =
KieBaseUtil.newKieBaseFromKieModuleWithAdditionalOptions(
+ kieModule, kieBaseTestConfiguration, SequentialOption.YES);
+ StatelessKieSession ksession = kbase.newStatelessKieSession();
+
+ final List<String> list = new ArrayList<>();
+ ksession.setGlobal("list", list);
+
+
ksession.execute(CommandFactory.newInsertElements(Arrays.asList(facts)));
+ return list;
+ }
+
+ // =====================================================================
+ // Model classes
+ // =====================================================================
+
+ public static class Order {
+
+ private final String id;
+ private int total;
+ private boolean discounted;
+
+ public Order(String id, int total) {
+ this.id = id;
+ this.total = total;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public int getTotal() {
+ return total;
+ }
+
+ public void setTotal(int total) {
+ this.total = total;
+ }
+
+ public boolean isDiscounted() {
+ return discounted;
+ }
+
+ public void setDiscounted(boolean discounted) {
+ this.discounted = discounted;
+ }
+ }
+
+ public static class Coupon {
+
+ private final String code;
+ private final int discount;
+
+ public Coupon(String code, int discount) {
+ this.code = code;
+ this.discount = discount;
+ }
+
+ public String getCode() {
+ return code;
+ }
+
+ public int getDiscount() {
+ return discount;
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]