Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for overrideable configuration settings such as feature flags. #58

Merged
merged 4 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ jobs:
distribution: 'temurin'
java-version: 17
- name: Build with Gradle
env:
testCONFIGIsDefault: true
run: ./gradlew build
99 changes: 99 additions & 0 deletions src/main/java/org/carlmontrobotics/Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package org.carlmontrobotics;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;

import edu.wpi.first.util.sendable.Sendable;
import edu.wpi.first.util.sendable.SendableBuilder;

abstract class Config implements Sendable {
public static final Config CONFIG = new Config() {
{
// Override config settings here, like this:
// this.exampleFlagEnabled = true;

// NOTE: PRs with overrides will NOT be merged because we don't want them
// polluting the master branch.
// Feel free to add them when testing, but remove them before pushing.
}
};

// Add additional config settings by declaring a protected field, and...
protected boolean exampleFlagEnabled = false;

// ...a public getter starting with "is" for booleans or "get" for other types.
// Do NOT remove this example. It is used by unit tests.
public boolean isExampleFlagEnabled() {
return exampleFlagEnabled;
}

// --- For clarity, place additional config settings ^above^ this line ---

private static class MethodResult {
String methodName = null;
Object retVal = null;
Object defaultRetVal = null;

MethodResult(String name, Object retVal, Object defaultRetval) {
this.methodName = name;
this.retVal = retVal;
this.defaultRetVal = defaultRetval;
}
}

private List<MethodResult> getMethodResults() {
var methodResults = new ArrayList<MethodResult>();
var defaultConfig = new Config() {
};
for (Method m : Config.class.getDeclaredMethods()) {
var name = m.getName();
if (!Modifier.isPublic(m.getModifiers()) || m.isSynthetic() || m.getParameterCount() != 0
|| !name.matches("^(get|is)[A-Z].*")) {
continue;
}
Object retVal = null;
try {
retVal = m.invoke(this);
} catch (Exception ex) {
retVal = ex;
}
Object defaultRetVal = null;
try {
defaultRetVal = m.invoke(defaultConfig);
} catch (Exception ex) {
defaultRetVal = ex;
}
methodResults.add(new MethodResult(name, retVal, defaultRetVal));
}
return methodResults;
}

@Override
public void initSendable(SendableBuilder builder) {
getMethodResults().forEach(mr -> {
if (!mr.retVal.equals(mr.defaultRetVal)) {
builder.publishConstString("%s()".formatted(mr.methodName),
String.format("%s (default is %s)", mr.retVal, mr.defaultRetVal));
}
});
}

@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
getMethodResults().forEach(mr -> {
if (!mr.retVal.equals(mr.defaultRetVal)) {
stringBuilder.append(
String.format("%s() returns %s (default is %s)", mr.methodName, mr.retVal, mr.defaultRetVal));
}
});
if (stringBuilder.isEmpty()) {
stringBuilder.append("Using default config values");
} else {
stringBuilder.insert(0, "WARNING: USING OVERRIDDEN CONFIG VALUES\n");
}
return stringBuilder.toString();
}
}
5 changes: 4 additions & 1 deletion src/main/java/org/carlmontrobotics/RobotContainer.java
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,10 @@ public class RobotContainer {

public RobotContainer() {
{
// SensorFactory.configureCamera();
// Put any configuration overrides to the dashboard and the terminal
SmartDashboard.putData("CONFIG overrides", Config.CONFIG);
System.out.println(Config.CONFIG);

SmartDashboard.setDefaultBoolean("babymode", babyMode);
SmartDashboard.setPersistent("babymode");
//safe auto setup... stuff in setupAutos() is not safe to run here - will break robot
Expand Down
81 changes: 81 additions & 0 deletions src/test/java/org/carlmontrobotics/ConfigTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package org.carlmontrobotics;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;

import edu.wpi.first.util.sendable.SendableBuilder;
import edu.wpi.first.wpilibj.smartdashboard.SendableBuilderImpl;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.HashMap;

public class ConfigTest {
@Test
void testIsExampleFlagEnabled() {
assertEquals(false, new Config() {
}.isExampleFlagEnabled());
assertEquals(true, new Config() {
{
this.exampleFlagEnabled = true;
}
}.isExampleFlagEnabled());
}

@Test
void testInitSendable() throws Exception {
var publishedStrings = new HashMap<String, String>();
try (SendableBuilder testBuilder = new SendableBuilderImpl() {
@Override
public void publishConstString(String key, String value) {
publishedStrings.put(key, value);
}
}) {
Config testConfig = new Config() {
};
testConfig.initSendable(testBuilder);
assertEquals(new HashMap<String, String>(), publishedStrings,
"A default config should not publish anything.");

testConfig = new Config() {
{
this.exampleFlagEnabled = true;
}
};
testConfig.initSendable(testBuilder);
assertEquals(new HashMap<String, String>() {
{
this.put("isExampleFlagEnabled()", "true (default is false)");
}
}, publishedStrings, "A config with overrides should publish what is overriden.");
}
}

@Test
public void testToString() {
assertEquals("Using default config values", new Config() {
}.toString());
assertEquals("WARNING: USING OVERRIDDEN CONFIG VALUES\nisExampleFlagEnabled() returns true (default is false)",
new Config() {
{
exampleFlagEnabled = true;
}
}.toString());
}

@Test
@EnabledIfEnvironmentVariable(named = "testCONFIGIsDefault", matches = "true", disabledReason = "not trying to modify GitHub master")
public void testNoConfigSettingsOverridden() throws Exception {
var publishedStrings = new HashMap<String, String>();
try (SendableBuilder testBuilder = new SendableBuilderImpl() {
@Override
public void publishConstString(String key, String value) {
publishedStrings.put(key, value);
}
}) {
Config.CONFIG.initSendable(testBuilder);
assertEquals(new HashMap<String, String>(), publishedStrings,
"Config.CONFIG must be empty to be on the master branch.");
}
}
}
Loading