From 49638335b9851e2559b5cbdc637bf8a031c74411 Mon Sep 17 00:00:00 2001 From: Ryan Cahoon Date: Wed, 2 Oct 2024 03:43:46 -0700 Subject: [PATCH] Rule action persistence policies --- .vscode/settings.json | 14 +- .../FunctionalInstantProcedure.java | 7 +- .../framework3/FunctionalProcedure.java | 7 +- .../java/com/team766/framework3/Rule.java | 115 +++++-- .../com/team766/framework3/RuleEngine.java | 13 +- .../team766/framework3/RulePersistence.java | 23 ++ .../team766/framework3/RuleEngineTest.java | 292 ++++++++++++++++-- .../java/com/team766/framework3/RuleTest.java | 37 ++- 8 files changed, 450 insertions(+), 58 deletions(-) create mode 100644 src/main/java/com/team766/framework3/RulePersistence.java diff --git a/.vscode/settings.json b/.vscode/settings.json index 714bc441..f25587d5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -33,5 +33,17 @@ "diffEditor.ignoreTrimWhitespace": false, "java.format.settings.url": "https://raw.githubusercontent.com/google/styleguide/gh-pages/eclipse-java-google-style.xml", "java.checkstyle.configuration": "${workspaceFolder}/.github/linters/checkstyle.xml", - "java.checkstyle.version": "9.3" + "java.checkstyle.version": "9.3", + "java.completion.favoriteStaticMembers": [ + "com.team766.framework3.RulePersistence.*", + "org.junit.Assert.*", + "org.junit.Assume.*", + "org.junit.jupiter.api.Assertions.*", + "org.junit.jupiter.api.Assumptions.*", + "org.junit.jupiter.api.DynamicContainer.*", + "org.junit.jupiter.api.DynamicTest.*", + "org.mockito.Mockito.*", + "org.mockito.ArgumentMatchers.*", + "org.mockito.Answers.*" + ] } diff --git a/src/main/java/com/team766/framework3/FunctionalInstantProcedure.java b/src/main/java/com/team766/framework3/FunctionalInstantProcedure.java index 2a78d02a..131369f2 100644 --- a/src/main/java/com/team766/framework3/FunctionalInstantProcedure.java +++ b/src/main/java/com/team766/framework3/FunctionalInstantProcedure.java @@ -6,7 +6,12 @@ public final class FunctionalInstantProcedure extends InstantProcedure { private final Runnable runnable; public FunctionalInstantProcedure(Set> reservations, Runnable runnable) { - super(runnable.toString(), reservations); + this(runnable.toString(), reservations, runnable); + } + + public FunctionalInstantProcedure( + String name, Set> reservations, Runnable runnable) { + super(name, reservations); this.runnable = runnable; } diff --git a/src/main/java/com/team766/framework3/FunctionalProcedure.java b/src/main/java/com/team766/framework3/FunctionalProcedure.java index 69d13c1b..141c416c 100644 --- a/src/main/java/com/team766/framework3/FunctionalProcedure.java +++ b/src/main/java/com/team766/framework3/FunctionalProcedure.java @@ -7,7 +7,12 @@ public final class FunctionalProcedure extends Procedure { private final Consumer runnable; public FunctionalProcedure(Set> reservations, Consumer runnable) { - super(runnable.toString(), reservations); + this(runnable.toString(), reservations, runnable); + } + + public FunctionalProcedure( + String name, Set> reservations, Consumer runnable) { + super(name, reservations); this.runnable = runnable; } diff --git a/src/main/java/com/team766/framework3/Rule.java b/src/main/java/com/team766/framework3/Rule.java index 5999e7f9..c8cfe6b8 100644 --- a/src/main/java/com/team766/framework3/Rule.java +++ b/src/main/java/com/team766/framework3/Rule.java @@ -5,6 +5,7 @@ import java.util.Map; import java.util.Set; import java.util.function.BooleanSupplier; +import java.util.function.Consumer; import java.util.function.Supplier; /** @@ -25,7 +26,7 @@ * public MyRules() { * // add rule to spin up the shooter when the boxop presses the right trigger on the gamepad * rules.add(Rule.create("spin up shooter", gamepad.getButton(InputConstants.XBOX_RT)). - * withNewlyTriggeringProcedure(() -> new ShooterSpin(shooter))); + * withOnTriggeringProcedure(ONCE_AND_HOLD, () -> new ShooterSpin(shooter))); * ... * } * } @@ -49,6 +50,14 @@ enum TriggerType { FINISHED } + /** Policy for canceling actions when the rule is in a given state. */ + enum Cancellation { + /** Do not cancel any previous actions. */ + DO_NOT_CANCEL, + /** Cancel the action previously scheduled when the rule was in the NEWLY state. */ + CANCEL_NEWLY_ACTION, + } + /** * Simple Builder for {@link Rule}s. Configure Rules via this Builder; these fields will be immutable * in the rule the Builder constructs. @@ -58,7 +67,8 @@ enum TriggerType { public static class Builder { private final String name; private final BooleanSupplier predicate; - private Supplier newlyTriggeringProcedure; + private Supplier onTriggeringProcedure; + private Cancellation cancellationOnFinish = Cancellation.DO_NOT_CANCEL; private Supplier finishedTriggeringProcedure; private Builder(String name, BooleanSupplier predicate) { @@ -66,16 +76,70 @@ private Builder(String name, BooleanSupplier predicate) { this.predicate = predicate; } + private void applyRulePersistence( + RulePersistence rulePersistence, Supplier action) { + switch (rulePersistence) { + case ONCE -> { + this.onTriggeringProcedure = action; + this.cancellationOnFinish = Cancellation.DO_NOT_CANCEL; + } + case ONCE_AND_HOLD -> { + this.onTriggeringProcedure = + () -> { + final Procedure procedure = action.get(); + return new FunctionalProcedure( + procedure.getName(), + procedure.reservations(), + context -> { + procedure.run(context); + context.waitFor(() -> false); + }); + }; + this.cancellationOnFinish = Cancellation.CANCEL_NEWLY_ACTION; + } + case REPEATEDLY -> { + this.onTriggeringProcedure = + () -> { + final Procedure procedure = action.get(); + return new FunctionalProcedure( + procedure.getName(), + procedure.reservations(), + context -> { + Procedure currentProcedure = procedure; + while (true) { + context.runSync(currentProcedure); + context.yield(); + currentProcedure = action.get(); + } + }); + }; + this.cancellationOnFinish = Cancellation.CANCEL_NEWLY_ACTION; + } + } + } + /** Specify a creator for the Procedure that should be run when this rule starts triggering. */ - public Builder withNewlyTriggeringProcedure(Supplier action) { - this.newlyTriggeringProcedure = action; + public Builder withOnTriggeringProcedure( + RulePersistence rulePersistence, Supplier action) { + applyRulePersistence(rulePersistence, action); return this; } - public Builder withNewlyTriggeringProcedure( - Set> reservations, Runnable action) { - this.newlyTriggeringProcedure = - () -> new FunctionalInstantProcedure(reservations, action); + /** Specify a creator for the Procedure that should be run when this rule starts triggering. */ + public Builder withOnTriggeringProcedure( + RulePersistence rulePersistence, Set> reservations, Runnable action) { + applyRulePersistence( + rulePersistence, () -> new FunctionalInstantProcedure(reservations, action)); + return this; + } + + /** Specify a creator for the Procedure that should be run when this rule starts triggering. */ + public Builder withOnTriggeringProcedure( + RulePersistence rulePersistence, + Set> reservations, + Consumer action) { + applyRulePersistence( + rulePersistence, () -> new FunctionalProcedure(reservations, action)); return this; } @@ -85,6 +149,7 @@ public Builder withFinishedTriggeringProcedure(Supplier action) { return this; } + /** Specify a creator for the Procedure that should be run when this rule was triggering before and is no longer triggering. */ public Builder withFinishedTriggeringProcedure( Set> reservations, Runnable action) { this.finishedTriggeringProcedure = @@ -94,7 +159,12 @@ public Builder withFinishedTriggeringProcedure( // called by {@link RuleEngine#addRule}. /* package */ Rule build() { - return new Rule(name, predicate, newlyTriggeringProcedure, finishedTriggeringProcedure); + return new Rule( + name, + predicate, + onTriggeringProcedure, + cancellationOnFinish, + finishedTriggeringProcedure); } } @@ -104,6 +174,7 @@ public Builder withFinishedTriggeringProcedure( Maps.newEnumMap(TriggerType.class); private final Map>> triggerReservations = Maps.newEnumMap(TriggerType.class); + private final Cancellation cancellationOnFinish; private TriggerType currentTriggerType = TriggerType.NONE; @@ -114,24 +185,27 @@ public static Builder create(String name, BooleanSupplier predicate) { private Rule( String name, BooleanSupplier predicate, - Supplier newlyTriggeringProcedure, + Supplier onTriggeringProcedure, + Cancellation cancellationOnFinish, Supplier finishedTriggeringProcedure) { if (predicate == null) { throw new IllegalArgumentException("Rule predicate has not been set."); } - if (newlyTriggeringProcedure == null) { - throw new IllegalArgumentException("Newly triggering Procedure is not defined."); + if (onTriggeringProcedure == null) { + throw new IllegalArgumentException("On-triggering Procedure is not defined."); } this.name = name; this.predicate = predicate; - if (newlyTriggeringProcedure != null) { - triggerProcedures.put(TriggerType.NEWLY, newlyTriggeringProcedure); + if (onTriggeringProcedure != null) { + triggerProcedures.put(TriggerType.NEWLY, onTriggeringProcedure); triggerReservations.put( - TriggerType.NEWLY, getReservationsForProcedure(newlyTriggeringProcedure)); + TriggerType.NEWLY, getReservationsForProcedure(onTriggeringProcedure)); } + this.cancellationOnFinish = cancellationOnFinish; + if (finishedTriggeringProcedure != null) { triggerProcedures.put(TriggerType.FINISHED, finishedTriggeringProcedure); triggerReservations.put( @@ -139,7 +213,7 @@ private Rule( } } - private Set> getReservationsForProcedure(Supplier supplier) { + private static Set> getReservationsForProcedure(Supplier supplier) { if (supplier != null) { Procedure procedure = supplier.get(); if (procedure != null) { @@ -182,10 +256,11 @@ public String getName() { } /* package */ Set> getMechanismsToReserve() { - if (triggerReservations.containsKey(currentTriggerType)) { - return triggerReservations.get(currentTriggerType); - } - return Collections.emptySet(); + return triggerReservations.getOrDefault(currentTriggerType, Collections.emptySet()); + } + + /* package */ Cancellation getCancellationOnFinish() { + return cancellationOnFinish; } /* package */ Procedure getProcedureToRun() { diff --git a/src/main/java/com/team766/framework3/RuleEngine.java b/src/main/java/com/team766/framework3/RuleEngine.java index 78d7ed3d..cab1c05b 100644 --- a/src/main/java/com/team766/framework3/RuleEngine.java +++ b/src/main/java/com/team766/framework3/RuleEngine.java @@ -86,7 +86,7 @@ public final void run() { rule.evaluate(); // see if the rule is triggering - Rule.TriggerType triggerType = rule.getCurrentTriggerType(); + final Rule.TriggerType triggerType = rule.getCurrentTriggerType(); if (triggerType != Rule.TriggerType.NONE) { log(Severity.INFO, "Rule " + rule.getName() + " triggering: " + triggerType); @@ -148,6 +148,17 @@ public final void run() { } // we're good to proceed + + if (triggerType == Rule.TriggerType.FINISHED + && rule.getCancellationOnFinish() + == Rule.Cancellation.CANCEL_NEWLY_ACTION) { + var newlyCommand = + ruleMap.inverse().get(new RuleAction(rule, Rule.TriggerType.NEWLY)); + if (newlyCommand != null) { + newlyCommand.cancel(); + } + } + Procedure procedure = rule.getProcedureToRun(); if (procedure == null) { continue; diff --git a/src/main/java/com/team766/framework3/RulePersistence.java b/src/main/java/com/team766/framework3/RulePersistence.java new file mode 100644 index 00000000..b6e5dafd --- /dev/null +++ b/src/main/java/com/team766/framework3/RulePersistence.java @@ -0,0 +1,23 @@ +package com.team766.framework3; + +/** + * Policies for how to handle a Rule's action when the action completes or the Rule stops triggering. + */ +public enum RulePersistence { + /** + * When the action completes, don't do anything. Any Mechanism reservations that the action held + * are released. Also, the action may continue running after the Rule stops triggering. + */ + ONCE, + /** + * When the action completes, don't do anything but retain the Mechanism reservations that the + * action held until the Rule stops triggering. If the Rule stops triggering before the action + * has completed, then the action will be terminated. + */ + ONCE_AND_HOLD, + /** + * When the action completes, start executing the action again, until the Rule stops triggering. + * The action will be terminated when the Rule stops triggering. + */ + REPEATEDLY, +} diff --git a/src/test/java/com/team766/framework3/RuleEngineTest.java b/src/test/java/com/team766/framework3/RuleEngineTest.java index aa496c90..0e909736 100644 --- a/src/test/java/com/team766/framework3/RuleEngineTest.java +++ b/src/test/java/com/team766/framework3/RuleEngineTest.java @@ -1,6 +1,10 @@ package com.team766.framework3; +import static com.team766.framework3.RulePersistence.ONCE; +import static com.team766.framework3.RulePersistence.ONCE_AND_HOLD; +import static com.team766.framework3.RulePersistence.REPEATEDLY; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -10,6 +14,7 @@ import edu.wpi.first.wpilibj2.command.CommandScheduler; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.BooleanSupplier; import org.junit.jupiter.api.Test; @@ -68,12 +73,12 @@ public void testAddRuleAndGetPriority() { { addRule( Rule.create("fm1_p0", new ScheduledPredicate(0)) - .withNewlyTriggeringProcedure( - () -> new FakeProcedure(2, Set.of(fm1)))); + .withOnTriggeringProcedure( + ONCE, () -> new FakeProcedure(2, Set.of(fm1)))); addRule( Rule.create("fm1_p1", new ScheduledPredicate(0)) - .withNewlyTriggeringProcedure( - () -> new FakeProcedure(2, Set.of(fm1)))); + .withOnTriggeringProcedure( + ONCE, () -> new FakeProcedure(2, Set.of(fm1)))); } }; @@ -97,12 +102,12 @@ public void testRunNonConflictingRules() { { addRule( Rule.create("fm1", new ScheduledPredicate(0)) - .withNewlyTriggeringProcedure( - () -> new FakeProcedure(2, Set.of(fm1)))); + .withOnTriggeringProcedure( + ONCE, () -> new FakeProcedure(2, Set.of(fm1)))); addRule( Rule.create("fm2", new ScheduledPredicate(0)) - .withNewlyTriggeringProcedure( - () -> new FakeProcedure(2, Set.of(fm2)))); + .withOnTriggeringProcedure( + ONCE, () -> new FakeProcedure(2, Set.of(fm2)))); } }; @@ -147,7 +152,8 @@ public void testFinishedProcedureBumpsNewlyProcedureForSameRule() { { addRule( Rule.create("fm1_p0", new ScheduledPredicate(0)) - .withNewlyTriggeringProcedure( + .withOnTriggeringProcedure( + ONCE, () -> new FakeProcedure( "fm1procnew_p0", 1, Set.of(fm1))) @@ -189,7 +195,8 @@ public void testRunRulePriorities() { { addRule( Rule.create("fm1_p0", new ScheduledPredicate(0)) - .withNewlyTriggeringProcedure( + .withOnTriggeringProcedure( + ONCE, () -> new FakeProcedure( "fm1proc_p0", @@ -197,7 +204,8 @@ public void testRunRulePriorities() { Set.of(fm1, fm2)))); addRule( Rule.create("fm1_p1", new PeriodicPredicate(2)) - .withNewlyTriggeringProcedure( + .withOnTriggeringProcedure( + ONCE, () -> new FakeProcedure( "fm1proc_p1", @@ -206,7 +214,8 @@ public void testRunRulePriorities() { addRule( Rule.create("fm3_p2", new ScheduledPredicate(0)) - .withNewlyTriggeringProcedure( + .withOnTriggeringProcedure( + ONCE, () -> new FakeProcedure( "fm3proc_p2", 0, Set.of(fm3)))); @@ -251,7 +260,8 @@ public void testRunHigherPriorityRuleStillBeingRun() { { addRule( Rule.create("fm1_p0", new ScheduledPredicate(0)) - .withNewlyTriggeringProcedure( + .withOnTriggeringProcedure( + ONCE, () -> new FakeProcedure( "fm1proc_p0", @@ -259,7 +269,8 @@ public void testRunHigherPriorityRuleStillBeingRun() { Set.of(fm1, fm2)))); addRule( Rule.create("fm1_p1", new ScheduledPredicate(1)) - .withNewlyTriggeringProcedure( + .withOnTriggeringProcedure( + ONCE, () -> new FakeProcedure( "fm1proc_p1", @@ -268,7 +279,8 @@ public void testRunHigherPriorityRuleStillBeingRun() { addRule( Rule.create("fm1_p2", new ScheduledPredicate(3)) - .withNewlyTriggeringProcedure( + .withOnTriggeringProcedure( + ONCE, () -> new FakeProcedure( "fm1proc_p2", @@ -322,7 +334,8 @@ public void testRunLowerPriorityRuleBumped() { { addRule( Rule.create("fm1_p0", new ScheduledPredicate(1)) - .withNewlyTriggeringProcedure( + .withOnTriggeringProcedure( + ONCE, () -> new FakeProcedure( "fm1proc_p0", @@ -330,7 +343,8 @@ public void testRunLowerPriorityRuleBumped() { Set.of(fm1, fm2)))); addRule( Rule.create("fm1_p1", new ScheduledPredicate(0)) - .withNewlyTriggeringProcedure( + .withOnTriggeringProcedure( + ONCE, () -> new FakeProcedure( "fm1proc_p1", @@ -364,13 +378,15 @@ public void testRuleResetIgnoredLowerPriorityRule() { { addRule( Rule.create("fm1_p0", new ScheduledPredicate(0)) - .withNewlyTriggeringProcedure( + .withOnTriggeringProcedure( + ONCE, () -> new FakeProcedure( "fm1procnew_p0", 2, Set.of(fm1)))); addRule( Rule.create("fm1_p1", new ScheduledPredicate(0)) - .withNewlyTriggeringProcedure( + .withOnTriggeringProcedure( + ONCE, () -> new FakeProcedure( "fm1procnew_p1", 1, Set.of(fm1))) @@ -405,13 +421,15 @@ public void testRuleResetIgnoredLowerPriorityRuleHigherPriorityRulePreviouslySch { addRule( Rule.create("fm1_p0", new ScheduledPredicate(0)) - .withNewlyTriggeringProcedure( + .withOnTriggeringProcedure( + ONCE, () -> new FakeProcedure( "fm1procnew_p0", 2, Set.of(fm1)))); addRule( Rule.create("fm1_p1", new ScheduledPredicate(1)) - .withNewlyTriggeringProcedure( + .withOnTriggeringProcedure( + ONCE, () -> new FakeProcedure( "fm1procnew_p1", 1, Set.of(fm1))) @@ -454,13 +472,15 @@ public void testRuleResetBumpedLowerPriorityRule() { { addRule( Rule.create("fm1_p0", new ScheduledPredicate(1)) - .withNewlyTriggeringProcedure( + .withOnTriggeringProcedure( + ONCE, () -> new FakeProcedure( "fm1procnew_p0", 2, Set.of(fm1)))); addRule( Rule.create("fm1_p1", new ScheduledPredicate(0)) - .withNewlyTriggeringProcedure( + .withOnTriggeringProcedure( + ONCE, () -> new FakeProcedure( "fm1procnew_p1", 2, Set.of(fm1))) @@ -495,7 +515,8 @@ public void testLowerPriorityRuleRunsWhenProcedureFromHigherPriorityRuleFinishes { addRule( Rule.create("fm1_p0", new ScheduledPredicate(0, 4)) - .withNewlyTriggeringProcedure( + .withOnTriggeringProcedure( + ONCE, () -> new FakeProcedure( "fm1procnew_p0", 0, Set.of(fm1))) @@ -505,7 +526,8 @@ public void testLowerPriorityRuleRunsWhenProcedureFromHigherPriorityRuleFinishes "fm1procfin_p0", 0, Set.of(fm1)))); addRule( Rule.create("fm1_p1", new ScheduledPredicate(1)) - .withNewlyTriggeringProcedure( + .withOnTriggeringProcedure( + ONCE, () -> new FakeProcedure( "fm1procnew_p1", 0, Set.of(fm1))) @@ -564,7 +586,8 @@ public void testRuleCalledAgainAfterBeingReset() { { addRule( Rule.create("fm1_p0", new ScheduledPredicate(1)) - .withNewlyTriggeringProcedure( + .withOnTriggeringProcedure( + ONCE, () -> new FakeProcedure( "fm1procnew_p0", 0, Set.of(fm1))) @@ -574,7 +597,8 @@ public void testRuleCalledAgainAfterBeingReset() { "fm1procfin_p0", 0, Set.of(fm1)))); addRule( Rule.create("fm1_p1", new ScheduledPredicate(0, 4)) - .withNewlyTriggeringProcedure( + .withOnTriggeringProcedure( + ONCE, () -> new FakeProcedure( "fm1procnew_p1", 1, Set.of(fm1))) @@ -634,7 +658,8 @@ public void testRuleResetPreventsFinishedForLongTrigger() { { addRule( Rule.create("fm1_p0", new ScheduledPredicate(1)) - .withNewlyTriggeringProcedure( + .withOnTriggeringProcedure( + ONCE, () -> new FakeProcedure( "fm1procnew_p0", 0, Set.of(fm1))) @@ -644,7 +669,8 @@ public void testRuleResetPreventsFinishedForLongTrigger() { "fm1procfin_p0", 0, Set.of(fm1)))); addRule( Rule.create("fm1_p1", new ScheduledPredicate(0, 3)) - .withNewlyTriggeringProcedure( + .withOnTriggeringProcedure( + ONCE, () -> new FakeProcedure( "fm1procnew_p1", 1, Set.of(fm1))) @@ -687,4 +713,212 @@ public void testRuleResetPreventsFinishedForLongTrigger() { step(); // 3 } + + /** Test ONCE RulePersistence policy */ + @Test + public void testOncePersistence() { + AtomicReference predicateEndsFirstProc = new AtomicReference<>(); + AtomicReference actionEndsFirstProc = new AtomicReference<>(); + RuleEngine myRules = + new RuleEngine() { + { + addRule( + Rule.create("predicate_ends_first", new ScheduledPredicate(0, 1)) + .withOnTriggeringProcedure( + ONCE, + () -> { + var proc = + new FakeProcedure( + "predicate_ends_first_proc", + 10, + Set.of(fm1)); + predicateEndsFirstProc.set(proc); + return proc; + })); + addRule( + Rule.create("action_ends_first", new ScheduledPredicate(0, 10)) + .withOnTriggeringProcedure( + ONCE, + () -> { + var proc = + new FakeProcedure( + "action_ends_first_proc", + 1, + Set.of(fm2)); + actionEndsFirstProc.set(proc); + return proc; + })); + } + }; + + myRules.run(); + + // check that both action Procedures are scheduled + Command cmd1 = CommandScheduler.getInstance().requiring(fm1); + assertNotNull(cmd1); + assertTrue(cmd1.getName().endsWith("predicate_ends_first_proc")); + Command cmd2 = CommandScheduler.getInstance().requiring(fm2); + assertNotNull(cmd2); + assertTrue(cmd2.getName().endsWith("action_ends_first_proc")); + + step(); + myRules.run(); + step(); + myRules.run(); + + // ONCE actions should be allowed to run after the rule has stopped triggering. + assertEquals(2, predicateEndsFirstProc.get().age()); + assertFalse(predicateEndsFirstProc.get().isEnded()); + cmd1 = CommandScheduler.getInstance().requiring(fm1); + assertNotNull(cmd1); + assertTrue(cmd1.getName().endsWith("predicate_ends_first_proc")); + + // If a ONCE action completes, it should end and mechanism reservations released. + assertTrue(actionEndsFirstProc.get().isEnded()); + cmd2 = CommandScheduler.getInstance().requiring(fm2); + assertNull(cmd2); + } + + /** Test ONCE_AND_HOLD RulePersistence policy */ + @Test + public void testOnceAndHoldPersistence() { + AtomicReference predicateEndsFirstProc = new AtomicReference<>(); + AtomicReference actionEndsFirstProc = new AtomicReference<>(); + RuleEngine myRules = + new RuleEngine() { + { + addRule( + Rule.create("predicate_ends_first", new ScheduledPredicate(0, 1)) + .withOnTriggeringProcedure( + ONCE_AND_HOLD, + () -> { + var proc = + new FakeProcedure( + "predicate_ends_first_proc", + 10, + Set.of(fm1)); + predicateEndsFirstProc.set(proc); + return proc; + })); + addRule( + Rule.create("action_ends_first", new ScheduledPredicate(0, 10)) + .withOnTriggeringProcedure( + ONCE_AND_HOLD, + () -> { + var proc = + new FakeProcedure( + "action_ends_first_proc", + 1, + Set.of(fm2)); + actionEndsFirstProc.set(proc); + return proc; + })); + } + }; + + myRules.run(); + + // check that both action Procedures are scheduled + Command cmd1 = CommandScheduler.getInstance().requiring(fm1); + assertNotNull(cmd1); + assertTrue(cmd1.getName().endsWith("predicate_ends_first_proc")); + Command cmd2 = CommandScheduler.getInstance().requiring(fm2); + assertNotNull(cmd2); + assertTrue(cmd2.getName().endsWith("action_ends_first_proc")); + + step(); + myRules.run(); + step(); + myRules.run(); + + // ONCE_AND_HOLD actions should be cancelled after the rule has stopped triggering. + assertTrue(predicateEndsFirstProc.get().isEnded()); + cmd1 = CommandScheduler.getInstance().requiring(fm1); + assertNull(cmd1); + + // If a ONCE_AND_HOLD action completes, it should end but mechanism reservations are + // retained. + assertTrue(actionEndsFirstProc.get().isEnded()); + cmd2 = CommandScheduler.getInstance().requiring(fm2); + assertNotNull(cmd2); + assertTrue(cmd2.getName().endsWith("action_ends_first_proc")); + } + + /** Test REPEATEDLY RulePersistence policy */ + @Test + public void testRepeatedlyPersistence() { + AtomicReference predicateEndsFirstProc = new AtomicReference<>(); + AtomicReference actionEndsFirstProc = new AtomicReference<>(); + RuleEngine myRules = + new RuleEngine() { + { + addRule( + Rule.create("predicate_ends_first", new ScheduledPredicate(0, 1)) + .withOnTriggeringProcedure( + REPEATEDLY, + () -> { + var proc = + new FakeProcedure( + "predicate_ends_first_proc", + 10, + Set.of(fm1)); + predicateEndsFirstProc.set(proc); + return proc; + })); + addRule( + Rule.create("action_ends_first", new ScheduledPredicate(0, 10)) + .withOnTriggeringProcedure( + REPEATEDLY, + () -> { + var proc = + new FakeProcedure( + "action_ends_first_proc", + 1, + Set.of(fm2)); + actionEndsFirstProc.set(proc); + return proc; + })); + } + }; + + myRules.run(); + + // check that both action Procedures are scheduled + Command cmd1 = CommandScheduler.getInstance().requiring(fm1); + assertNotNull(cmd1); + assertTrue(cmd1.getName().endsWith("predicate_ends_first_proc")); + Command cmd2 = CommandScheduler.getInstance().requiring(fm2); + assertNotNull(cmd2); + assertTrue(cmd2.getName().endsWith("action_ends_first_proc")); + + step(); + myRules.run(); + step(); + myRules.run(); + step(); + + // REPEATEDLY actions should be cancelled after the rule has stopped triggering. + assertTrue(predicateEndsFirstProc.get().isEnded()); + cmd1 = CommandScheduler.getInstance().requiring(fm1); + assertNull(cmd1); + + // If a REPEATEDLY action completes, another instance should be started. + assertFalse(actionEndsFirstProc.get().isEnded()); + cmd2 = CommandScheduler.getInstance().requiring(fm2); + assertNotNull(cmd2); + assertTrue(cmd2.getName().endsWith("action_ends_first_proc")); + + final FakeProcedure previousActionInstance = actionEndsFirstProc.get(); + + myRules.run(); + step(); + myRules.run(); + step(); + + assertTrue(previousActionInstance.isEnded()); + assertFalse(actionEndsFirstProc.get().isEnded()); + cmd2 = CommandScheduler.getInstance().requiring(fm2); + assertNotNull(cmd2); + assertTrue(cmd2.getName().endsWith("action_ends_first_proc")); + } } diff --git a/src/test/java/com/team766/framework3/RuleTest.java b/src/test/java/com/team766/framework3/RuleTest.java index 3b7730a8..0b35bb6f 100644 --- a/src/test/java/com/team766/framework3/RuleTest.java +++ b/src/test/java/com/team766/framework3/RuleTest.java @@ -1,9 +1,13 @@ package com.team766.framework3; +import static com.team766.framework3.RulePersistence.ONCE; +import static com.team766.framework3.RulePersistence.ONCE_AND_HOLD; +import static com.team766.framework3.RulePersistence.REPEATEDLY; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import com.team766.framework3.Rule.Cancellation; import com.team766.framework3.Rule.TriggerType; import java.util.Collections; import java.util.Set; @@ -43,7 +47,7 @@ protected void setUp() {} public void testCreate() { Rule alwaysTrue = Rule.create("always true", () -> true) - .withNewlyTriggeringProcedure(() -> Procedure.NO_OP) + .withOnTriggeringProcedure(ONCE, () -> Procedure.NO_OP) .build(); assertNotNull(alwaysTrue); assertEquals("always true", alwaysTrue.getName()); @@ -54,7 +58,7 @@ public void testEvaluate() { // start with simple test of a NONE->NEWLY->CONTINUING->CONTINUING sequence Rule alwaysTrue = Rule.create("always true", () -> true) - .withNewlyTriggeringProcedure(() -> Procedure.NO_OP) + .withOnTriggeringProcedure(ONCE, () -> Procedure.NO_OP) .build(); assertEquals(Rule.TriggerType.NONE, alwaysTrue.getCurrentTriggerType()); alwaysTrue.evaluate(); @@ -67,7 +71,7 @@ public void testEvaluate() { // test a full cycle: NONE->NEWLY->CONTINUING->FINISHED->NONE->NEWLY->... Rule duckDuckGooseGoose = Rule.create("duck duck goose goose", new DuckDuckGooseGoosePredicate()) - .withNewlyTriggeringProcedure(() -> Procedure.NO_OP) + .withOnTriggeringProcedure(ONCE, () -> Procedure.NO_OP) .build(); assertEquals(Rule.TriggerType.NONE, duckDuckGooseGoose.getCurrentTriggerType()); duckDuckGooseGoose.evaluate(); @@ -82,6 +86,29 @@ public void testEvaluate() { assertEquals(TriggerType.NEWLY, duckDuckGooseGoose.getCurrentTriggerType()); } + @Test + public void testGetCancellation() { + Rule ruleWithOnce = + Rule.create("always true", new DuckDuckGooseGoosePredicate()) + .withOnTriggeringProcedure(ONCE, () -> Procedure.NO_OP) + .build(); + assertEquals(Cancellation.DO_NOT_CANCEL, ruleWithOnce.getCancellationOnFinish()); + + Rule ruleWithOnceAndHold = + Rule.create("always true", new DuckDuckGooseGoosePredicate()) + .withOnTriggeringProcedure(ONCE_AND_HOLD, () -> Procedure.NO_OP) + .build(); + assertEquals( + Cancellation.CANCEL_NEWLY_ACTION, ruleWithOnceAndHold.getCancellationOnFinish()); + + Rule ruleWithRepeatedly = + Rule.create("always true", new DuckDuckGooseGoosePredicate()) + .withOnTriggeringProcedure(REPEATEDLY, () -> Procedure.NO_OP) + .build(); + assertEquals( + Cancellation.CANCEL_NEWLY_ACTION, ruleWithRepeatedly.getCancellationOnFinish()); + } + @Test public void testGetMechanismsToReserve() { final Set> newlyMechanisms = @@ -90,7 +117,7 @@ public void testGetMechanismsToReserve() { Rule duckDuckGooseGoose = Rule.create("duck duck goose goose", new DuckDuckGooseGoosePredicate()) - .withNewlyTriggeringProcedure(newlyMechanisms, () -> {}) + .withOnTriggeringProcedure(ONCE, newlyMechanisms, () -> {}) .withFinishedTriggeringProcedure(finishedMechanisms, () -> {}) .build(); @@ -122,7 +149,7 @@ public void testGetMechanismsToReserve() { public void testGetProcedureToRun() { Rule duckDuckGooseGoose = Rule.create("duck duck goose goose", new DuckDuckGooseGoosePredicate()) - .withNewlyTriggeringProcedure(() -> new TrivialProcedure("newly")) + .withOnTriggeringProcedure(ONCE, () -> new TrivialProcedure("newly")) .withFinishedTriggeringProcedure(() -> new TrivialProcedure("finished")) .build();