mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-06-19 00:41:43 +00:00
[epilogue] Autogenerate nicer data names by default, not just raw element names (#7167)
eg "getFoo()" will now be logged as "Foo", or "m_leftMotor" as "Left Motor" It is now a compilation error to reuse the same logged name for multiple elements (since whatever is declared last would overwrite anything logged before it) Do not log record fields (just use the accessors). This also fixes an issue where records could never be logged due to identical member and accessor names Also skips toString, hashCode, and clone methods when generating loggers
This commit is contained in:
@@ -61,15 +61,46 @@ public abstract class ElementHandler {
|
||||
* @return the name specified in the {@link Logged @Logged} annotation on the element, if present;
|
||||
* otherwise, the field or method's name with no modifications
|
||||
*/
|
||||
public String loggedName(Element element) {
|
||||
var elementName = element.getSimpleName().toString();
|
||||
var config = element.getAnnotation(Logged.class);
|
||||
public static String loggedName(Element element) {
|
||||
var elementConfig = element.getAnnotation(Logged.class);
|
||||
|
||||
if (config != null && !config.name().isBlank()) {
|
||||
return config.name();
|
||||
} else {
|
||||
return elementName;
|
||||
// Use the name provided on the logged element, if one is present
|
||||
if (elementConfig != null && !elementConfig.name().isBlank()) {
|
||||
return elementConfig.name();
|
||||
}
|
||||
|
||||
var config = elementConfig;
|
||||
|
||||
if (config == null) {
|
||||
// Look up the parent class configuration
|
||||
// We assume one is present, since logged elements should only be found if the enclosing class
|
||||
// is @Logged itself
|
||||
Logged parentConfig = null;
|
||||
for (var parent = element.getEnclosingElement();
|
||||
parent != null;
|
||||
parent = parent.getEnclosingElement()) {
|
||||
parentConfig = parent.getAnnotation(Logged.class);
|
||||
if (parentConfig != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
config = parentConfig;
|
||||
}
|
||||
|
||||
if (config == null) {
|
||||
// Uh oh
|
||||
throw new IllegalStateException(
|
||||
"Could not generate a name for element "
|
||||
+ element
|
||||
+ " without a @Logged annotation AND without being contained within a class with a @Logged annotation!\n\nOpen an issue at https://github.com/wpilibsuite/allwpilib/issues and include a copy of the file that caused this error.");
|
||||
}
|
||||
|
||||
var elementName = element.getSimpleName().toString();
|
||||
return switch (config.defaultNaming()) {
|
||||
case USE_CODE_NAME -> elementName;
|
||||
case USE_HUMAN_NAME -> StringUtils.toHumanName(elementName);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,23 +7,36 @@ package edu.wpi.first.epilogue.processor;
|
||||
import static java.util.stream.Collectors.groupingBy;
|
||||
import static java.util.stream.Collectors.toList;
|
||||
|
||||
import com.sun.source.tree.IdentifierTree;
|
||||
import com.sun.source.tree.ReturnTree;
|
||||
import com.sun.source.util.SimpleTreeVisitor;
|
||||
import com.sun.source.util.Trees;
|
||||
import edu.wpi.first.epilogue.Logged;
|
||||
import edu.wpi.first.epilogue.NotLogged;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.ExecutableElement;
|
||||
import javax.lang.model.element.Modifier;
|
||||
import javax.lang.model.element.Name;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.element.VariableElement;
|
||||
import javax.tools.Diagnostic;
|
||||
|
||||
/** Generates logger class files for {@link Logged @Logged}-annotated classes. */
|
||||
public class LoggerGenerator {
|
||||
public static final Predicate<ExecutableElement> kIsBuiltInJavaMethod =
|
||||
LoggerGenerator::isBuiltInJavaMethod;
|
||||
private final ProcessingEnvironment m_processingEnv;
|
||||
private final List<ElementHandler> m_handlers;
|
||||
|
||||
@@ -36,6 +49,19 @@ public class LoggerGenerator {
|
||||
return e.getAnnotation(NotLogged.class) == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a method is a method declared in java.lang.Object that should not be logged.
|
||||
*
|
||||
* @param e the method to check
|
||||
* @return true if the method is toString, hashCode, or clone; false otherwise
|
||||
*/
|
||||
private static boolean isBuiltInJavaMethod(ExecutableElement e) {
|
||||
Name name = e.getSimpleName();
|
||||
return name.contentEquals("toString")
|
||||
|| name.contentEquals("hashCode")
|
||||
|| name.contentEquals("clone");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the logger class used to handle data objects of the given type. The generated logger
|
||||
* class will subclass from {@link edu.wpi.first.epilogue.logging.ClassSpecificLogger} and
|
||||
@@ -53,17 +79,23 @@ public class LoggerGenerator {
|
||||
Predicate<Element> optedIn =
|
||||
e -> !requireExplicitOptIn || e.getAnnotation(Logged.class) != null;
|
||||
|
||||
var fieldsToLog =
|
||||
clazz.getEnclosedElements().stream()
|
||||
.filter(e -> e instanceof VariableElement)
|
||||
.map(e -> (VariableElement) e)
|
||||
.filter(notSkipped)
|
||||
.filter(optedIn)
|
||||
.filter(e -> !e.getModifiers().contains(Modifier.STATIC))
|
||||
.filter(this::isLoggable)
|
||||
.toList();
|
||||
List<VariableElement> fieldsToLog;
|
||||
if (Objects.equals(clazz.getSuperclass().toString(), "java.lang.Record")) {
|
||||
// Do not log record members - just use the accessor methods
|
||||
fieldsToLog = List.of();
|
||||
} else {
|
||||
fieldsToLog =
|
||||
clazz.getEnclosedElements().stream()
|
||||
.filter(e -> e instanceof VariableElement)
|
||||
.map(e -> (VariableElement) e)
|
||||
.filter(notSkipped)
|
||||
.filter(optedIn)
|
||||
.filter(e -> !e.getModifiers().contains(Modifier.STATIC))
|
||||
.filter(this::isLoggable)
|
||||
.toList();
|
||||
}
|
||||
|
||||
var methodsToLog =
|
||||
List<ExecutableElement> methodsToLog =
|
||||
clazz.getEnclosedElements().stream()
|
||||
.filter(e -> e instanceof ExecutableElement)
|
||||
.map(e -> (ExecutableElement) e)
|
||||
@@ -73,9 +105,51 @@ public class LoggerGenerator {
|
||||
.filter(e -> e.getModifiers().contains(Modifier.PUBLIC))
|
||||
.filter(e -> e.getParameters().isEmpty())
|
||||
.filter(e -> e.getReceiverType() != null)
|
||||
.filter(kIsBuiltInJavaMethod.negate())
|
||||
.filter(this::isLoggable)
|
||||
.filter(e -> !isSimpleGetterMethodForLoggedField(e, fieldsToLog))
|
||||
.toList();
|
||||
|
||||
// Validate no name collisions
|
||||
Map<String, List<Element>> usedNames =
|
||||
Stream.concat(fieldsToLog.stream(), methodsToLog.stream())
|
||||
.reduce(
|
||||
new HashMap<>(),
|
||||
(map, element) -> {
|
||||
String name = ElementHandler.loggedName(element);
|
||||
map.computeIfAbsent(name, _k -> new ArrayList<>()).add(element);
|
||||
|
||||
return map;
|
||||
},
|
||||
(left, right) -> {
|
||||
left.putAll(right);
|
||||
return left;
|
||||
});
|
||||
|
||||
usedNames.forEach(
|
||||
(name, elements) -> {
|
||||
if (elements.size() > 1) {
|
||||
// Collisions!
|
||||
for (Element conflictingElement : elements) {
|
||||
String conflicts =
|
||||
elements.stream()
|
||||
.filter(e -> !e.equals(conflictingElement))
|
||||
.map(e -> e.getEnclosingElement().getSimpleName() + "." + e)
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
m_processingEnv
|
||||
.getMessager()
|
||||
.printMessage(
|
||||
Diagnostic.Kind.ERROR,
|
||||
"[EPILOGUE] Conflicting name detected: \""
|
||||
+ name
|
||||
+ "\" is also used by "
|
||||
+ conflicts,
|
||||
conflictingElement);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
writeLoggerFile(clazz.getQualifiedName().toString(), config, fieldsToLog, methodsToLog);
|
||||
}
|
||||
|
||||
@@ -242,4 +316,55 @@ public class LoggerGenerator {
|
||||
private boolean isLoggable(Element element) {
|
||||
return m_handlers.stream().anyMatch(h -> h.isLoggable(element));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a method is a simple "getter" method for a field in the given list. Here, we define
|
||||
* "getter" as a method with a single return statement that references the name of a field, with
|
||||
* no other expressions. `getX() { return x; }` would be considered a "getter" method, while
|
||||
* `getX() { return x.clone(); }` would not be. Note that the method name is irrelevant; only the
|
||||
* method body is checked.
|
||||
*
|
||||
* @param ex the method to check
|
||||
* @param fieldsToLog the fields that will already be logged
|
||||
* @return true if the method is a simple "getter" method, false otherwise
|
||||
*/
|
||||
private boolean isSimpleGetterMethodForLoggedField(
|
||||
ExecutableElement ex, List<VariableElement> fieldsToLog) {
|
||||
var trees = Trees.instance(m_processingEnv);
|
||||
var methodTree = trees.getTree(ex);
|
||||
|
||||
if (methodTree == null) {
|
||||
// probably a record's synthetic reader method
|
||||
return false;
|
||||
}
|
||||
|
||||
if (methodTree.getBody() == null) {
|
||||
// Abstract or native method, can't be determined to be a getter
|
||||
return false;
|
||||
}
|
||||
|
||||
var statements = methodTree.getBody().getStatements();
|
||||
if (statements.size() != 1) {
|
||||
// More complex than a simple `return m_field` statement
|
||||
return false;
|
||||
}
|
||||
|
||||
var statement = statements.get(0);
|
||||
if (!(statement instanceof ReturnTree ret)) {
|
||||
// Shouldn't get here, since we've already filtered for methods that return a value
|
||||
// and with a single statement - that one statement should be the return
|
||||
return false;
|
||||
}
|
||||
|
||||
var returnExpression = ret.getExpression();
|
||||
return returnExpression.accept(
|
||||
new SimpleTreeVisitor<Boolean, Void>(false) {
|
||||
@Override
|
||||
public Boolean visitIdentifier(IdentifierTree identifier, Void unused) {
|
||||
return fieldsToLog.stream()
|
||||
.anyMatch(v -> v.getSimpleName().contentEquals(identifier.getName()));
|
||||
}
|
||||
},
|
||||
null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
package edu.wpi.first.epilogue.processor;
|
||||
|
||||
import edu.wpi.first.epilogue.Logged;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
|
||||
public final class StringUtils {
|
||||
@@ -59,6 +62,33 @@ public final class StringUtils {
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a camel-cased string like "fooBar" into individual words like ["foo", "Bar"].
|
||||
*
|
||||
* @param camelCasedString the camel-cased string to split
|
||||
* @return the individual words in the input
|
||||
*/
|
||||
public static List<String> splitToWords(CharSequence camelCasedString) {
|
||||
// Implementation from https://stackoverflow.com/a/2560017, refactored for readability
|
||||
|
||||
// Uppercase letter not followed by the first letter of the next word
|
||||
// This allows for splitting "IOLayer" into "IO" and "Layer"
|
||||
String penultimateUppercaseLetter = "(?<=[A-Z])(?=[A-Z][a-z])";
|
||||
|
||||
// Any character that's NOT an uppercase letter, immediately followed by an uppercase letter
|
||||
// This allows for splitting "fooBar" into "foo" and "Bar", or "123Bang" into "123" and "Bang"
|
||||
String lastNonUppercaseLetter = "(?<=[^A-Z])(?=[A-Z])";
|
||||
|
||||
// The final letter in a sequence, followed by a non-alpha character like a number or underscore
|
||||
// This allows for splitting "foo123" into "foo" and "123"
|
||||
String finalLetter = "(?<=[A-Za-z])(?=[^A-Za-z])";
|
||||
|
||||
String regex =
|
||||
String.format("%s|%s|%s", penultimateUppercaseLetter, lastNonUppercaseLetter, finalLetter);
|
||||
|
||||
return Arrays.asList(camelCasedString.toString().split(regex));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the field used to hold a logger for data of the given type.
|
||||
*
|
||||
@@ -107,4 +137,31 @@ public final class StringUtils {
|
||||
|
||||
return loggerClassName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a camelCase element name to separate words, removing common field and method name
|
||||
* prefixes like "m_" and "get".
|
||||
*
|
||||
* @param elementName the camelcased element name
|
||||
* @return the name split into separate words and sanitized
|
||||
*/
|
||||
public static String toHumanName(String elementName) {
|
||||
// Delete common field prefixes (k_name, m_name, s_name)
|
||||
var sanitizedName = elementName.replaceFirst("^[msk]_", "");
|
||||
|
||||
// Drop leading "k" prefix from fields
|
||||
// (though normally these should be static, and thus not logged)
|
||||
if (sanitizedName.matches("^k[A-Z].*$")) {
|
||||
sanitizedName = sanitizedName.substring(1);
|
||||
}
|
||||
|
||||
// Drop leading "get" from accessor methods
|
||||
if (sanitizedName.matches("^get[A-Z].*$")) {
|
||||
sanitizedName = sanitizedName.substring(3);
|
||||
}
|
||||
|
||||
return splitToWords(sanitizedName).stream()
|
||||
.map(StringUtils::capitalize)
|
||||
.collect(Collectors.joining(" "));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,9 +301,9 @@ class AnnotationProcessorTest {
|
||||
byte[] arr1; // Should be logged
|
||||
byte[][] arr2; // Should not be logged
|
||||
|
||||
public byte getX() { return x; }
|
||||
public byte[] getArr1() { return arr1; }
|
||||
public byte[][] getArr2() { return arr2; }
|
||||
public byte getX() { return 0; }
|
||||
public byte[] getArr1() { return null; }
|
||||
public byte[][] getArr2() { return null; }
|
||||
}
|
||||
""";
|
||||
|
||||
@@ -348,9 +348,9 @@ class AnnotationProcessorTest {
|
||||
char[] arr1; // Should not be logged
|
||||
char[][] arr2; // Should not be logged
|
||||
|
||||
public char getX() { return x; }
|
||||
public char[] getArr1() { return arr1; }
|
||||
public char[][] getArr2() { return arr2; }
|
||||
public char getX() { return 'x'; }
|
||||
public char[] getArr1() { return null; }
|
||||
public char[][] getArr2() { return null; }
|
||||
}
|
||||
""";
|
||||
|
||||
@@ -393,9 +393,9 @@ class AnnotationProcessorTest {
|
||||
short[] arr1; // Should not be logged
|
||||
short[][] arr2; // Should not be logged
|
||||
|
||||
public short getX() { return x; }
|
||||
public short[] getArr1() { return arr1; }
|
||||
public short[][] getArr2() { return arr2; }
|
||||
public short getX() { return 0; }
|
||||
public short[] getArr1() { return null; }
|
||||
public short[][] getArr2() { return null; }
|
||||
}
|
||||
""";
|
||||
|
||||
@@ -438,9 +438,9 @@ class AnnotationProcessorTest {
|
||||
int[] arr1; // Should be logged
|
||||
int[][] arr2; // Should not be logged
|
||||
|
||||
public int getX() { return x; }
|
||||
public int[] getArr1() { return arr1; }
|
||||
public int[][] getArr2() { return arr2; }
|
||||
public int getX() { return 0; }
|
||||
public int[] getArr1() { return null; }
|
||||
public int[][] getArr2() { return null; }
|
||||
}
|
||||
""";
|
||||
|
||||
@@ -485,9 +485,9 @@ class AnnotationProcessorTest {
|
||||
long[] arr1; // Should be logged
|
||||
long[][] arr2; // Should not be logged
|
||||
|
||||
public long getX() { return x; }
|
||||
public long[] getArr1() { return arr1; }
|
||||
public long[][] getArr2() { return arr2; }
|
||||
public long getX() { return 0; }
|
||||
public long[] getArr1() { return null; }
|
||||
public long[][] getArr2() { return null; }
|
||||
}
|
||||
""";
|
||||
|
||||
@@ -532,9 +532,9 @@ class AnnotationProcessorTest {
|
||||
float[] arr1; // Should be logged
|
||||
float[][] arr2; // Should not be logged
|
||||
|
||||
public float getX() { return x; }
|
||||
public float[] getArr1() { return arr1; }
|
||||
public float[][] getArr2() { return arr2; }
|
||||
public float getX() { return 0; }
|
||||
public float[] getArr1() { return null; }
|
||||
public float[][] getArr2() { return null; }
|
||||
}
|
||||
""";
|
||||
|
||||
@@ -582,9 +582,9 @@ class AnnotationProcessorTest {
|
||||
double[][] arr2; // Should not be logged
|
||||
List<Double> list; // Should not be logged
|
||||
|
||||
public double getX() { return x; }
|
||||
public double[] getArr1() { return arr1; }
|
||||
public double[][] getArr2() { return arr2; }
|
||||
public double getX() { return 0; }
|
||||
public double[] getArr1() { return null; }
|
||||
public double[][] getArr2() { return null; }
|
||||
}
|
||||
""";
|
||||
|
||||
@@ -631,9 +631,9 @@ class AnnotationProcessorTest {
|
||||
boolean[][] arr2; // Should not be logged
|
||||
List<Boolean> list; // Should not be logged
|
||||
|
||||
public boolean getX() { return x; }
|
||||
public boolean[] getArr1() { return arr1; }
|
||||
public boolean[][] getArr2() { return arr2; }
|
||||
public boolean getX() { return false; }
|
||||
public boolean[] getArr1() { return null; }
|
||||
public boolean[][] getArr2() { return null; }
|
||||
}
|
||||
""";
|
||||
|
||||
@@ -681,9 +681,9 @@ class AnnotationProcessorTest {
|
||||
String[][] arr2; // Should not be logged
|
||||
List<String> list; // Should be logged
|
||||
|
||||
public String getX() { return x; }
|
||||
public String[] getArr1() { return arr1; }
|
||||
public String[][] getArr2() { return arr2; }
|
||||
public String getX() { return null; }
|
||||
public String[] getArr1() { return null; }
|
||||
public String[][] getArr2() { return null; }
|
||||
}
|
||||
""";
|
||||
|
||||
@@ -740,9 +740,9 @@ class AnnotationProcessorTest {
|
||||
Structable[][] arr2; // Should not be logged
|
||||
List<Structable> list; // Should be logged
|
||||
|
||||
public Structable getX() { return x; }
|
||||
public Structable[] getArr1() { return arr1; }
|
||||
public Structable[][] getArr2() { return arr2; }
|
||||
public Structable getX() { return null; }
|
||||
public Structable[] getArr1() { return null; }
|
||||
public Structable[][] getArr2() { return null; }
|
||||
}
|
||||
""";
|
||||
|
||||
@@ -1138,6 +1138,187 @@ class AnnotationProcessorTest {
|
||||
message);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loggingRecords() {
|
||||
String source =
|
||||
"""
|
||||
package edu.wpi.first.epilogue;
|
||||
|
||||
@Logged
|
||||
record Example(double x, double y) { }
|
||||
""";
|
||||
|
||||
String expectedRootLogger =
|
||||
"""
|
||||
package edu.wpi.first.epilogue;
|
||||
|
||||
import edu.wpi.first.epilogue.Logged;
|
||||
import edu.wpi.first.epilogue.Epilogue;
|
||||
import edu.wpi.first.epilogue.logging.ClassSpecificLogger;
|
||||
import edu.wpi.first.epilogue.logging.DataLogger;
|
||||
|
||||
public class ExampleLogger extends ClassSpecificLogger<Example> {
|
||||
public ExampleLogger() {
|
||||
super(Example.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(DataLogger dataLogger, Example object) {
|
||||
if (Epilogue.shouldLog(Logged.Importance.DEBUG)) {
|
||||
dataLogger.log("x", object.x());
|
||||
dataLogger.log("y", object.y());
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
assertLoggerGenerates(source, expectedRootLogger);
|
||||
}
|
||||
|
||||
@Test
|
||||
void errorsOnFieldNameConflicts() {
|
||||
String source =
|
||||
"""
|
||||
package edu.wpi.first.epilogue;
|
||||
|
||||
@Logged
|
||||
class Example {
|
||||
@Logged(name = "Custom Name") double x;
|
||||
@Logged(name = "Custom Name") double y;
|
||||
@Logged(name = "Custom Name") double z;
|
||||
}
|
||||
""";
|
||||
|
||||
Compilation compilation =
|
||||
javac()
|
||||
.withProcessors(new AnnotationProcessor())
|
||||
.compile(JavaFileObjects.forSourceString("edu.wpi.first.epilogue.Example", source));
|
||||
|
||||
assertThat(compilation).failed();
|
||||
assertThat(compilation).hadErrorCount(3);
|
||||
|
||||
List<Diagnostic<? extends JavaFileObject>> errors = compilation.errors();
|
||||
assertAll(
|
||||
() ->
|
||||
assertCompilationError(
|
||||
"[EPILOGUE] Conflicting name detected: \"Custom Name\" is also used by Example.y, Example.z",
|
||||
5,
|
||||
40,
|
||||
errors.get(0)),
|
||||
() ->
|
||||
assertCompilationError(
|
||||
"[EPILOGUE] Conflicting name detected: \"Custom Name\" is also used by Example.x, Example.z",
|
||||
6,
|
||||
40,
|
||||
errors.get(1)),
|
||||
() ->
|
||||
assertCompilationError(
|
||||
"[EPILOGUE] Conflicting name detected: \"Custom Name\" is also used by Example.x, Example.y",
|
||||
7,
|
||||
40,
|
||||
errors.get(2)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotErrorOnGetterMethod() {
|
||||
String source =
|
||||
"""
|
||||
package edu.wpi.first.epilogue;
|
||||
|
||||
@Logged
|
||||
class Example {
|
||||
double x;
|
||||
public double x() { return x; }
|
||||
public double getX() { return x; }
|
||||
public double aTotallyArbitraryNameForAnAccessorMethod() { return x; }
|
||||
public double withANoOpTransform() { return x + 0; }
|
||||
public double withTemp() { var temp = x; return temp; }
|
||||
}
|
||||
""";
|
||||
|
||||
String expectedRootLogger =
|
||||
"""
|
||||
package edu.wpi.first.epilogue;
|
||||
|
||||
import edu.wpi.first.epilogue.Logged;
|
||||
import edu.wpi.first.epilogue.Epilogue;
|
||||
import edu.wpi.first.epilogue.logging.ClassSpecificLogger;
|
||||
import edu.wpi.first.epilogue.logging.DataLogger;
|
||||
|
||||
public class ExampleLogger extends ClassSpecificLogger<Example> {
|
||||
public ExampleLogger() {
|
||||
super(Example.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(DataLogger dataLogger, Example object) {
|
||||
if (Epilogue.shouldLog(Logged.Importance.DEBUG)) {
|
||||
dataLogger.log("x", object.x);
|
||||
dataLogger.log("withANoOpTransform", object.withANoOpTransform());
|
||||
dataLogger.log("withTemp", object.withTemp());
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
assertLoggerGenerates(source, expectedRootLogger);
|
||||
}
|
||||
|
||||
@Test
|
||||
void configuredDefaultNaming() {
|
||||
String source =
|
||||
"""
|
||||
package edu.wpi.first.epilogue;
|
||||
|
||||
@Logged(defaultNaming = Logged.Naming.USE_HUMAN_NAME)
|
||||
class Example {
|
||||
double m_memberPrefix;
|
||||
double kConstantPrefix;
|
||||
double k_otherConstantPrefix;
|
||||
double s_otherPrefix;
|
||||
|
||||
public double getTheGetterMethod() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Logged(defaultNaming = Logged.Naming.USE_CODE_NAME)
|
||||
public double optedOut() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
String expectedRootLogger =
|
||||
"""
|
||||
package edu.wpi.first.epilogue;
|
||||
|
||||
import edu.wpi.first.epilogue.Logged;
|
||||
import edu.wpi.first.epilogue.Epilogue;
|
||||
import edu.wpi.first.epilogue.logging.ClassSpecificLogger;
|
||||
import edu.wpi.first.epilogue.logging.DataLogger;
|
||||
|
||||
public class ExampleLogger extends ClassSpecificLogger<Example> {
|
||||
public ExampleLogger() {
|
||||
super(Example.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(DataLogger dataLogger, Example object) {
|
||||
if (Epilogue.shouldLog(Logged.Importance.DEBUG)) {
|
||||
dataLogger.log("Member Prefix", object.m_memberPrefix);
|
||||
dataLogger.log("Constant Prefix", object.kConstantPrefix);
|
||||
dataLogger.log("Other Constant Prefix", object.k_otherConstantPrefix);
|
||||
dataLogger.log("Other Prefix", object.s_otherPrefix);
|
||||
dataLogger.log("The Getter Method", object.getTheGetterMethod());
|
||||
dataLogger.log("optedOut", object.optedOut());
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
assertLoggerGenerates(source, expectedRootLogger);
|
||||
}
|
||||
|
||||
private void assertCompilationError(
|
||||
String message, long lineNumber, long col, Diagnostic<? extends JavaFileObject> diagnostic) {
|
||||
assertAll(
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
|
||||
package edu.wpi.first.epilogue.processor;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertAll;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class StringUtilsTest {
|
||||
@@ -16,4 +18,22 @@ class StringUtilsTest {
|
||||
assertEquals("fooBar", StringUtils.lowerCamelCase("FooBar"));
|
||||
assertEquals("allcaps", StringUtils.lowerCamelCase("ALLCAPS"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void splitToWords() {
|
||||
assertAll(
|
||||
() -> assertEquals(List.of("IO", "Logger"), StringUtils.splitToWords("IOLogger")),
|
||||
() -> assertEquals(List.of("LED", "Subsystem"), StringUtils.splitToWords("LEDSubsystem")),
|
||||
() -> assertEquals(List.of("Foo", "Bar"), StringUtils.splitToWords("FooBar")),
|
||||
() -> assertEquals(List.of("ALLCAPS"), StringUtils.splitToWords("ALLCAPS")),
|
||||
() ->
|
||||
assertEquals(List.of("k", "First", "Second"), StringUtils.splitToWords("kFirstSecond")),
|
||||
() ->
|
||||
assertEquals(
|
||||
List.of("there", "Is", "A", "Number", "123", "In", "Here", "VERSION", "456"),
|
||||
StringUtils.splitToWords("thereIsANumber123InHereVERSION456")),
|
||||
() ->
|
||||
assertEquals(
|
||||
List.of("get", "First", "Second"), StringUtils.splitToWords("getFirstSecond")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,4 +89,39 @@ public @interface Logged {
|
||||
* @return the importance of the annotated element
|
||||
*/
|
||||
Importance importance() default Importance.DEBUG;
|
||||
|
||||
/**
|
||||
* Different behaviors for how Epilogue will generate the names of logged data points. This only
|
||||
* applies to automatically generated names; any specific name provided with {@link #name()} will
|
||||
* take precedence over an automatically generated name.
|
||||
*/
|
||||
enum Naming {
|
||||
/**
|
||||
* Sets the default naming strategy to use the name of the element as it appears in source code.
|
||||
* For example, a field {@code double m_x} would be labeled as {@code "m_x"} by default, and a
|
||||
* {@code getX()} accessor would be labeled as {@code "getX"}.
|
||||
*/
|
||||
USE_CODE_NAME,
|
||||
|
||||
/**
|
||||
* Sets the default naming strategy to use a human-readable name based on the name of the name
|
||||
* of the element as it appears in source code. For example, a field {@code double m_x} would be
|
||||
* labeled as {@code "X"} by default, and a {@code getX()} accessor would also be labeled as
|
||||
* {@code "X"}. Because logged names must be unique, this configuration would fail to compile
|
||||
* and require either one of the fields to be excluded from logs (which, for simple accessors,
|
||||
* would be ideal to avoid duplicate data), or to rename one or both elements so the logged data
|
||||
* fields would have unique names.
|
||||
*/
|
||||
USE_HUMAN_NAME
|
||||
}
|
||||
|
||||
/**
|
||||
* The default naming behavior to use. Defaults to {@link Naming#USE_CODE_NAME}, which uses the
|
||||
* raw code name directly in logs. Any configuration of the {@link #name()} attribute on logged
|
||||
* fields and methods will take precedence over an automatically generated name.
|
||||
*
|
||||
* @return the naming strategy for and annotated field or method, or the default naming strategy
|
||||
* for all logged fields and methods in an annotated class
|
||||
*/
|
||||
Naming defaultNaming() default Naming.USE_CODE_NAME;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user