From 49da6203a10c42026e1ce3e42b501ee6cc14ec1f Mon Sep 17 00:00:00 2001 From: Kevin Cooney Date: Sun, 17 Mar 2024 18:57:04 -0700 Subject: [PATCH] Refactor so features are evaluated when commands are returned Evalating the features inside the command was problematic, since replacing a Command expression with an expression that could return a feature-controlled flag would change the requirements of the resulting Command. --- .../feature/FeatureControlledCommand.java | 72 +++++++++ .../lib2813/feature/FeatureIdentifier.java | 15 ++ .../lib2813/feature/FeatureRegistry.java | 78 +++------- .../team2813/lib2813/feature/Features.java | 145 +++++++++++++----- .../lib2813/feature/FakeFeatures.java | 35 +++++ .../lib2813/feature/FeatureRegistryTest.java | 89 +++-------- .../lib2813/feature/FeaturesTest.java | 70 ++++++--- 7 files changed, 316 insertions(+), 188 deletions(-) create mode 100644 lib/src/main/java/com/team2813/lib2813/feature/FeatureControlledCommand.java create mode 100644 lib/src/test/java/com/team2813/lib2813/feature/FakeFeatures.java diff --git a/lib/src/main/java/com/team2813/lib2813/feature/FeatureControlledCommand.java b/lib/src/main/java/com/team2813/lib2813/feature/FeatureControlledCommand.java new file mode 100644 index 0000000..3072811 --- /dev/null +++ b/lib/src/main/java/com/team2813/lib2813/feature/FeatureControlledCommand.java @@ -0,0 +1,72 @@ +package com.team2813.lib2813.feature; + +import static java.util.Objects.requireNonNull; + +import edu.wpi.first.util.sendable.SendableBuilder; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.Commands; +import edu.wpi.first.wpilibj2.command.WrapperCommand; + +import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** A command that is commandSelected via one or more features. */ +public final class FeatureControlledCommand extends WrapperCommand { + private final boolean commandSelected; + private final String expression; + + /** If the features are all disabled, use the provided command. */ + public Command otherwise(Supplier commandSupplier) { + if (commandSelected) { + return this; + } + return new FeatureControlledCommand(commandSupplier.get(), String.format("not (%s)", expression)); + } + + /** If the features are disabled, and the following feature is commandSelected, use the provided command. */ + public & FeatureIdentifier> FeatureControlledCommand elseIfEnabled(T feature, Supplier commandSupplier) { + if (commandSelected) { + return this; + } + return ifAllEnabled(commandSupplier, Collections.singleton(feature), + s -> String.format("not %s and (%s)", expression, s)); + } + + String getExpression() { + return expression; + } + + static FeatureControlledCommand ifAllEnabled( + Supplier commandSupplier, Collection features) { + return ifAllEnabled(commandSupplier, features, s -> s); + } + + private static FeatureControlledCommand ifAllEnabled( + Supplier commandSupplier, Collection features, Function expressionBuilder) { + requireNonNull(commandSupplier, "commandSupplier cannot be null"); + requireNonNull(features, "features cannot be null"); + + String expression = expressionBuilder.apply( + features.stream().map(FeatureIdentifier::name).collect(Collectors.joining(", "))); + + if (features.stream().noneMatch(Objects::isNull) && + features.stream().allMatch(FeatureIdentifier::enabled)) { + return new FeatureControlledCommand(commandSupplier.get(), expression); + } + return new FeatureControlledCommand(null, "not " + expression); + } + + private FeatureControlledCommand(Command command, String expression) { + super(command == null ? Commands.none() : command); + this.commandSelected = (command != null); + this.expression = expression; + } + + @Override + public void initSendable(SendableBuilder builder) { + super.initSendable(builder); + builder.publishConstString("expression", expression); + } +} diff --git a/lib/src/main/java/com/team2813/lib2813/feature/FeatureIdentifier.java b/lib/src/main/java/com/team2813/lib2813/feature/FeatureIdentifier.java index 8a806eb..6aae008 100644 --- a/lib/src/main/java/com/team2813/lib2813/feature/FeatureIdentifier.java +++ b/lib/src/main/java/com/team2813/lib2813/feature/FeatureIdentifier.java @@ -1,5 +1,10 @@ package com.team2813.lib2813.feature; +import java.util.Collections; +import java.util.function.Supplier; + +import edu.wpi.first.wpilibj2.command.Command; + /** Mix-in interface for features. This should be implemented by an enum. */ public interface FeatureIdentifier { @@ -22,4 +27,14 @@ enum FeatureBehavior { default boolean enabled() { return FeatureRegistry.getInstance().enabled(this); } + + /** + * Decorates the command if this feature is enabled. + * + *

If the feature is disabled, returns a do-nothing command. + */ + default FeatureControlledCommand ifEnabled(Supplier commandSupplier) { + return FeatureControlledCommand.ifAllEnabled( + commandSupplier, Collections.singleton(this)); + } } diff --git a/lib/src/main/java/com/team2813/lib2813/feature/FeatureRegistry.java b/lib/src/main/java/com/team2813/lib2813/feature/FeatureRegistry.java index 42f69bb..1828915 100644 --- a/lib/src/main/java/com/team2813/lib2813/feature/FeatureRegistry.java +++ b/lib/src/main/java/com/team2813/lib2813/feature/FeatureRegistry.java @@ -1,14 +1,8 @@ package com.team2813.lib2813.feature; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import java.util.function.BooleanSupplier; -import java.util.stream.Stream; +import java.util.stream.Collectors; import com.team2813.lib2813.feature.FeatureIdentifier.FeatureBehavior; @@ -20,58 +14,19 @@ /** Container for features that can be enabled at runtime. */ final class FeatureRegistry { private final Map registeredFeatures = new ConcurrentHashMap<>(); + private final ShuffleboardTab shuffleboardTab; /** Package-scope constructor (for testing) */ - FeatureRegistry() {} + FeatureRegistry(String shuffleboardTabName) { + this.shuffleboardTab = Shuffleboard.getTab(shuffleboardTabName); + } public static FeatureRegistry getInstance() { return SingletonHolder.instance; } private static class SingletonHolder { - static final FeatureRegistry instance = new FeatureRegistry(); - } - - /** - * Creates a {@link BooleanSupplier} that returns {@code true} iff all the given features are enabled. - * - * @param first A feature identifier (if {@code null}, the returned supplier will always return {@code false}). - * @param rest Zero or more additional feature identifiers (if {@code null} or contains {@code null} values, the - * returned supplier will always return {@code false}). - */ - @SafeVarargs - final & FeatureIdentifier> BooleanSupplier asSupplier(T first, T... rest) { - if (first == null || rest == null) { - return () -> false; - } - - List featureIdentifiers = new ArrayList<>(rest.length + 1); - featureIdentifiers.add(first); - featureIdentifiers.addAll(Arrays.asList(rest)); - return asSupplier(featureIdentifiers); - } - - & FeatureIdentifier> BooleanSupplier asSupplier(Collection featureIdentifiers) { - if (featureIdentifiers == null || featureIdentifiers.stream().anyMatch(Objects::isNull)) { - return () -> false; - } - - List features = featureIdentifiers.stream().map(this::getFeature).toList(); - - return () -> features.stream().allMatch(Feature::enabled); - } - - @SafeVarargs - final & FeatureIdentifier> boolean allEnabled(T first, T... rest) { - if (first == null || rest == null || Stream.of(rest).anyMatch(Objects::isNull)) { - return false; - } - - if (!getFeature(first).enabled()) { - return false; - } - - return Stream.of(rest).map(this::getFeature).allMatch(Feature::enabled); + static final FeatureRegistry instance = new FeatureRegistry("Features"); } boolean enabled(FeatureIdentifier id) { @@ -82,9 +37,16 @@ Feature getFeature(FeatureIdentifier id) { return registeredFeatures.computeIfAbsent(id, Feature::new); } - static final class Feature { + Object getState() { + return registeredFeatures.entrySet().stream() + .filter(entry -> entry.getValue().changed()) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + } + + final class Feature { final SimpleWidget widget; - private static final ShuffleboardTab shuffleboardTab = Shuffleboard.getTab("Features"); + final boolean initiallyEnabled; Feature(FeatureIdentifier id) { String name = String.format("%s.%s", id.getClass().getName(), id.name()); @@ -93,19 +55,23 @@ static final class Feature { } FeatureBehavior behavior = id.behavior(); - boolean enabled = (behavior == FeatureBehavior.INITIALLY_ENABLED); + initiallyEnabled = (behavior == FeatureBehavior.INITIALLY_ENABLED); boolean alwaysDisabled = ( behavior == null || behavior == FeatureBehavior.ALWAYS_DISABLED); if (alwaysDisabled) { shuffleboardTab.addBoolean(name, () -> false).withWidget(BuiltInWidgets.kBooleanBox); widget = null; } else { - widget = shuffleboardTab.add(name, enabled).withWidget(BuiltInWidgets.kToggleSwitch); + widget = shuffleboardTab.add(name, initiallyEnabled).withWidget(BuiltInWidgets.kToggleSwitch); } } boolean enabled() { return widget != null && widget.getEntry().getBoolean(false); } + + boolean changed() { + return enabled() != initiallyEnabled; + } } } diff --git a/lib/src/main/java/com/team2813/lib2813/feature/Features.java b/lib/src/main/java/com/team2813/lib2813/feature/Features.java index c7659c1..a5594a1 100644 --- a/lib/src/main/java/com/team2813/lib2813/feature/Features.java +++ b/lib/src/main/java/com/team2813/lib2813/feature/Features.java @@ -1,13 +1,11 @@ package com.team2813.lib2813.feature; -import edu.wpi.first.util.sendable.SendableBuilder; import edu.wpi.first.wpilibj2.command.*; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import static java.util.Objects.requireNonNull; +import java.util.*; +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; +import java.util.stream.Stream; public class Features { private Features() { @@ -15,9 +13,9 @@ private Features() { } /** - * Decorates the command to only run if all the provided features were enabled when the command is initialized. + * Decorates the command to only run if all the provided features were enabled. * - * @param command The command to decorate + * @param commandSupplier Called to create the command. * @param first A feature identifier (if {@code null}, the command will not be run). * @param rest Zero or more additional feature identifiers (if {@code null} or contains {@code null} values, the * command will not be run). @@ -26,47 +24,122 @@ private Features() { * @throws NullPointerException if {@code command} is {@code null} */ @SafeVarargs - public static & FeatureIdentifier> ConditionalCommand whenAllEnabled( - Command command, T first, T... rest) { - requireNonNull(command, "command cannot be null"); - if (first == null || rest == null) { - return new ConditionalCommand(Commands.none(), Commands.none(), () -> false); + public static & FeatureIdentifier> FeatureControlledCommand ifAllEnabled( + Supplier commandSupplier, T first, T... rest) { + return FeatureControlledCommand.ifAllEnabled(commandSupplier, nonNull(first, rest)); + } + + /** + * Returns {@code true} iff all the given features are currently enabled. + * + * @param featureIdentifiers Feature identifiers (if {@code null} or contains {@code null} values, the + * returned value will be {@code false}). + */ + public static & FeatureIdentifier> boolean allEnabled(Collection featureIdentifiers) { + if (featureIdentifiers == null || featureIdentifiers.stream().anyMatch(Objects::isNull)) { + return false; } - List features = new ArrayList<>(rest.length + 1); - features.add(first); - features.addAll(Arrays.asList(rest)); - return new FeatureControlledCommand<>(command, features); + FeatureRegistry registry = FeatureRegistry.getInstance(); + return featureIdentifiers.stream().map(registry::getFeature).allMatch(FeatureRegistry.Feature::enabled); } - private static class FeatureControlledCommand & FeatureIdentifier> extends ConditionalCommand { - final List features; + /** + * Returns {@code true} iff all the given features are currently enabled. + * + * @param first A feature identifier (if {@code null}, this method will return {@code false}). + * @param rest Zero or more additional feature identifiers (if {@code null} or contains {@code null} values, + * this method will return {@code false}). + */ + @SafeVarargs + public static & FeatureIdentifier> boolean allEnabled(T first, T... rest) { + if (first == null || rest == null || Stream.of(rest).anyMatch(Objects::isNull)) { + return false; + } - FeatureControlledCommand(Command command, List features) { - super(command, Commands.none(), FeatureRegistry.getInstance().asSupplier(features)); - this.features = new ArrayList<>(features); + FeatureRegistry registry = FeatureRegistry.getInstance(); + if (!registry.getFeature(first).enabled()) { + return false; } + return Stream.of(rest).map(registry::getFeature).allMatch(FeatureRegistry.Feature::enabled); + } - @Override - public void initSendable(SendableBuilder builder) { - super.initSendable(builder); - builder.addStringArrayProperty( - "features", - () -> features.stream().map(FeatureIdentifier::name).toArray(String[]::new), - null); + /** + * Creates a {@link BooleanSupplier} that returns {@code true} iff all the given features are enabled. + * + * @param featureIdentifiers Feature identifiers (if {@code null} or contains {@code null} values, the + * returned supplier will always return {@code false}). + */ + public static & FeatureIdentifier> BooleanSupplier asSupplier(Collection featureIdentifiers) { + if (featureIdentifiers == null || featureIdentifiers.stream().anyMatch(Objects::isNull)) { + return () -> false; } + + FeatureRegistry registry = FeatureRegistry.getInstance(); + List features = featureIdentifiers.stream().map(registry::getFeature).toList(); + return () -> features.stream().allMatch(FeatureRegistry.Feature::enabled); } /** - * Returns {@code True} iff all the given features are enabled. + * Creates a {@link BooleanSupplier} that returns {@code true} iff all the given features are enabled. * - *

To see determine if a single feature is enabled, use {@link FeatureIdentifier#enabled()} + * @param first A feature identifier (if {@code null}, the returned supplier will always return {@code false}). + * @param rest Zero or more additional feature identifiers (if {@code null} or contains {@code null} values, the + * returned supplier will always return {@code false}). + */ + @SafeVarargs + public static & FeatureIdentifier> BooleanSupplier asSupplier(T first, T... rest) { + if (first == null || rest == null) { + return () -> false; + } + + List featureIdentifiers = new ArrayList<>(rest.length + 1); + featureIdentifiers.add(first); + featureIdentifiers.addAll(Arrays.asList(rest)); + return asSupplier(featureIdentifiers); + } + + /** + * Gets the state of all features * - * @param first A feature identifier (if {@code null}, the command will not be run). - * @param rest Zero or more additional feature identifiers (if {@code null} or contains {@code null} values, the - * command will not be run). + *

This returned value encodes all features that have been updated from their + * default values. This can be used to determine if feature values have changed + * over time (for example, to reset the system if the feature values have changed + * since the last time the robot was enabled). */ + public static Object getState() { + return FeatureRegistry.getInstance().getState(); + } + @SafeVarargs - public static & FeatureIdentifier> boolean allEnabled(T first, T... rest) { - return FeatureRegistry.getInstance().allEnabled(first, rest); + private static & FeatureIdentifier> Collection nonNull(T first, T... rest) { + if (first == null || rest == null) { + return Collections.singleton(NullFeature.INSTANCE); + } + + List featureIdentifiers = new ArrayList<>(rest.length + 1); + featureIdentifiers.add(first); + Stream.of(rest).map(NullFeature::nullToNullFeature).forEach(featureIdentifiers::add); + return featureIdentifiers; + } + + private static class NullFeature implements FeatureIdentifier { + static final NullFeature INSTANCE = new NullFeature(); + + static FeatureIdentifier nullToNullFeature(FeatureIdentifier featureIdentifier) { + if (featureIdentifier == null) { + return INSTANCE; + } + return featureIdentifier; + } + + @Override + public String name() { + return "NULL"; + } + + @Override + public FeatureBehavior behavior() { + return FeatureBehavior.ALWAYS_DISABLED; + } } } diff --git a/lib/src/test/java/com/team2813/lib2813/feature/FakeFeatures.java b/lib/src/test/java/com/team2813/lib2813/feature/FakeFeatures.java new file mode 100644 index 0000000..f1b306d --- /dev/null +++ b/lib/src/test/java/com/team2813/lib2813/feature/FakeFeatures.java @@ -0,0 +1,35 @@ +package com.team2813.lib2813.feature; + +class FakeFeatures { + + public static void reset(FeatureRegistry registry) { + for (FakeFeature feature : FakeFeature.values()) { + switch (feature.featureBehavior) { + case INITIALLY_DISABLED: + registry.getFeature(feature).widget.getEntry().setBoolean(false); + break; + case INITIALLY_ENABLED: + registry.getFeature(feature).widget.getEntry().setBoolean(true); + break; + } + } + + } + + public enum FakeFeature implements FeatureIdentifier { + ALWAYS_DISABLED(FeatureBehavior.ALWAYS_DISABLED), + INITIALLY_DISABLED(FeatureBehavior.INITIALLY_DISABLED), + INITIALLY_ENABLED(FeatureBehavior.INITIALLY_ENABLED); + + private final FeatureBehavior featureBehavior; + + FakeFeature(FeatureBehavior featureBehavior) { + this.featureBehavior = featureBehavior; + } + + @Override + public FeatureBehavior behavior() { + return featureBehavior; + } + } +} diff --git a/lib/src/test/java/com/team2813/lib2813/feature/FeatureRegistryTest.java b/lib/src/test/java/com/team2813/lib2813/feature/FeatureRegistryTest.java index 44ff642..d916156 100644 --- a/lib/src/test/java/com/team2813/lib2813/feature/FeatureRegistryTest.java +++ b/lib/src/test/java/com/team2813/lib2813/feature/FeatureRegistryTest.java @@ -3,25 +3,30 @@ import static org.junit.Assert.*; import org.junit.After; +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TestName; -import java.util.Arrays; +import com.team2813.lib2813.feature.FakeFeatures.FakeFeature; -public class FeatureRegistryTest { - static final FeatureRegistry registry = new FeatureRegistry(); +public final class FeatureRegistryTest { + private FeatureRegistry registry; + + @Rule + public final TestName testName = new TestName(); + + @Before + public void createFeatureRegistry() { + String shuffleboardTabName = String.format("%s.%s", + FeatureRegistryTest.class.getSimpleName(), + testName.getMethodName()); + registry = new FeatureRegistry(shuffleboardTabName); + } @After public void resetFeatures() { - for (FakeFeature feature : FakeFeature.values()) { - switch (feature.featureBehavior) { - case INITIALLY_DISABLED: - registry.getFeature(feature).widget.getEntry().setBoolean(false); - break; - case INITIALLY_ENABLED: - registry.getFeature(feature).widget.getEntry().setBoolean(true); - break; - } - } + FakeFeatures.reset(registry); } @Test @@ -56,62 +61,4 @@ public void disable() { registry.getFeature(feature).widget.getEntry().setBoolean(false); assertFalse(registry.enabled(feature)); } - - @Test - public void allEnabled() { - FakeFeature feature1 = FakeFeature.INITIALLY_DISABLED; - FakeFeature feature2 = FakeFeature.INITIALLY_ENABLED; - assertFalse(registry.allEnabled(feature1, feature2)); - registry.getFeature(feature1).widget.getEntry().setBoolean(true); - assertTrue(registry.allEnabled(feature1, feature2)); - } - - @Test - public void asSupplier() { - FakeFeature feature1 = FakeFeature.INITIALLY_DISABLED; - FakeFeature feature2 = FakeFeature.INITIALLY_ENABLED; - assertFalse(registry.asSupplier(feature1, feature2).getAsBoolean()); - registry.getFeature(feature1).widget.getEntry().setBoolean(true); - assertTrue(registry.asSupplier(feature1, feature2).getAsBoolean()); - assertFalse(registry.asSupplier(feature1, feature2, FakeFeature.ALWAYS_DISABLED).getAsBoolean()); - } - - @Test - public void asSupplier_nullFeature() { - FakeFeature feature = FakeFeature.INITIALLY_ENABLED; - assertFalse(registry.asSupplier(feature, (FakeFeature) null).getAsBoolean()); - assertFalse(registry.asSupplier(null, feature).getAsBoolean()); - } - - @Test - public void collectionAsSupplier() { - FakeFeature feature1 = FakeFeature.INITIALLY_DISABLED; - FakeFeature feature2 = FakeFeature.INITIALLY_ENABLED; - assertFalse(registry.asSupplier(Arrays.asList(feature1, feature2)).getAsBoolean()); - registry.getFeature(feature1).widget.getEntry().setBoolean(true); - assertTrue(registry.asSupplier(Arrays.asList(feature1, feature2)).getAsBoolean()); - } - - @Test - public void collectionAsSupplier_nullFeature() { - FakeFeature feature = FakeFeature.INITIALLY_ENABLED; - assertFalse(registry.asSupplier(Arrays.asList(feature, null)).getAsBoolean()); - } - - enum FakeFeature implements FeatureIdentifier { - ALWAYS_DISABLED(FeatureBehavior.ALWAYS_DISABLED), - INITIALLY_DISABLED(FeatureBehavior.INITIALLY_DISABLED), - INITIALLY_ENABLED(FeatureBehavior.INITIALLY_ENABLED); - - private final FeatureBehavior featureBehavior; - - FakeFeature(FeatureBehavior featureBehavior) { - this.featureBehavior = featureBehavior; - } - - @Override - public FeatureBehavior behavior() { - return featureBehavior; - } - } } diff --git a/lib/src/test/java/com/team2813/lib2813/feature/FeaturesTest.java b/lib/src/test/java/com/team2813/lib2813/feature/FeaturesTest.java index c3a9858..e690fda 100644 --- a/lib/src/test/java/com/team2813/lib2813/feature/FeaturesTest.java +++ b/lib/src/test/java/com/team2813/lib2813/feature/FeaturesTest.java @@ -11,7 +11,11 @@ import org.junit.BeforeClass; import org.junit.Test; -public class FeaturesTest { +import java.util.Arrays; + +import com.team2813.lib2813.feature.FakeFeatures.FakeFeature; + +public final class FeaturesTest { static boolean wasEnabled; static final FeatureRegistry registry = FeatureRegistry.getInstance(); static final CommandScheduler scheduler = CommandScheduler.getInstance(); @@ -35,22 +39,13 @@ public static void resetDriverStation() { @After public void resetFeatures() { - for (FakeFeature feature : FakeFeature.values()) { - switch (feature.featureBehavior) { - case INITIALLY_DISABLED: - registry.getFeature(feature).widget.getEntry().setBoolean(false); - break; - case INITIALLY_ENABLED: - registry.getFeature(feature).widget.getEntry().setBoolean(true); - break; - } - } + FakeFeatures.reset(registry); } @Test public void whenAllEnabled_someFeaturesDisabled() { SimpleCommand command = new SimpleCommand(); - Command whenAllEnabled = Features.whenAllEnabled(command, FakeFeature.INITIALLY_ENABLED, FakeFeature.INITIALLY_DISABLED); + Command whenAllEnabled = Features.ifAllEnabled(() -> command, FakeFeature.INITIALLY_ENABLED, FakeFeature.INITIALLY_DISABLED); scheduler.schedule(whenAllEnabled); scheduler.run(); assertFalse(command.ran); @@ -60,7 +55,7 @@ public void whenAllEnabled_someFeaturesDisabled() { public void whenAllEnabled_allFeaturesEnabled() { SimpleCommand command = new SimpleCommand(); registry.getFeature(FakeFeature.INITIALLY_DISABLED).widget.getEntry().setBoolean(true); - Command whenAllEnabled = Features.whenAllEnabled(command, FakeFeature.INITIALLY_ENABLED, FakeFeature.INITIALLY_DISABLED); + Command whenAllEnabled = Features.ifAllEnabled(() -> command, FakeFeature.INITIALLY_ENABLED, FakeFeature.INITIALLY_DISABLED); scheduler.schedule(whenAllEnabled); scheduler.run(); assertTrue(whenAllEnabled.isScheduled()); @@ -76,20 +71,45 @@ public void execute() { } } - enum FakeFeature implements FeatureIdentifier { - ALWAYS_DISABLED(FeatureBehavior.ALWAYS_DISABLED), - INITIALLY_DISABLED(FeatureBehavior.INITIALLY_DISABLED), - INITIALLY_ENABLED(FeatureBehavior.INITIALLY_ENABLED); - private final FeatureBehavior featureBehavior; + @Test + public void allEnabled() { + FakeFeature feature1 = FakeFeature.INITIALLY_DISABLED; + FakeFeature feature2 = FakeFeature.INITIALLY_ENABLED; + assertFalse(Features.allEnabled(feature1, feature2)); + registry.getFeature(feature1).widget.getEntry().setBoolean(true); + assertTrue(Features.allEnabled(feature1, feature2)); + } - FakeFeature(FeatureBehavior featureBehavior) { - this.featureBehavior = featureBehavior; - } + @Test + public void asSupplier() { + FakeFeature feature1 = FakeFeature.INITIALLY_DISABLED; + FakeFeature feature2 = FakeFeature.INITIALLY_ENABLED; + assertFalse(Features.asSupplier(feature1, feature2).getAsBoolean()); + registry.getFeature(feature1).widget.getEntry().setBoolean(true); + assertTrue(Features.asSupplier(feature1, feature2).getAsBoolean()); + assertFalse(Features.asSupplier(feature1, feature2, FakeFeature.ALWAYS_DISABLED).getAsBoolean()); + } - @Override - public FeatureBehavior behavior() { - return featureBehavior; - } + @Test + public void asSupplier_nullFeature() { + FakeFeature feature = FakeFeature.INITIALLY_ENABLED; + assertFalse(Features.asSupplier(feature, (FakeFeature) null).getAsBoolean()); + assertFalse(Features.asSupplier(null, feature).getAsBoolean()); + } + + @Test + public void collectionAsSupplier() { + FakeFeature feature1 = FakeFeature.INITIALLY_DISABLED; + FakeFeature feature2 = FakeFeature.INITIALLY_ENABLED; + assertFalse(Features.asSupplier(Arrays.asList(feature1, feature2)).getAsBoolean()); + registry.getFeature(feature1).widget.getEntry().setBoolean(true); + assertTrue(Features.asSupplier(Arrays.asList(feature1, feature2)).getAsBoolean()); + } + + @Test + public void collectionAsSupplier_nullFeature() { + FakeFeature feature = FakeFeature.INITIALLY_ENABLED; + assertFalse(Features.asSupplier(Arrays.asList(feature, null)).getAsBoolean()); } }