Skip to content

Commit

Permalink
Merge pull request #58 from DeepBlueRobotics/add-config-support
Browse files Browse the repository at this point in the history
Add support for overrideable configuration settings such as feature flags.
  • Loading branch information
brettle authored Apr 26, 2024
2 parents 4accbf8 + e4323c7 commit 2b7f1fa
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 1 deletion.
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.");
}
}
}

0 comments on commit 2b7f1fa

Please sign in to comment.