Skip to content

Commit

Permalink
Add support for overrideable configuration settings such as feature f…
Browse files Browse the repository at this point in the history
…lags.

Also modify the CI build task to fail if any settings are overridden.
  • Loading branch information
brettle committed Apr 25, 2024
1 parent 62c09ea commit 27d3bd2
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 0 deletions.
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
95 changes: 95 additions & 0 deletions src/main/java/org/carlmontrobotics/Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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;
}
};

// 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();
}
}
4 changes: 4 additions & 0 deletions src/main/java/org/carlmontrobotics/RobotContainer.java
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ public class RobotContainer {

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

//safe auto setup... stuff in setupAutos() is not safe to run here - will break robot
registerAutoCommands();
SmartDashboard.putData(autoSelector);
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 testCONFIGIsDefault() 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 27d3bd2

Please sign in to comment.