diff --git a/epilogue-processor/src/main/java/org/wpilib/epilogue/processor/AnnotationProcessor.java b/epilogue-processor/src/main/java/org/wpilib/epilogue/processor/AnnotationProcessor.java index fb264967cba..f6637367a0b 100644 --- a/epilogue-processor/src/main/java/org/wpilib/epilogue/processor/AnnotationProcessor.java +++ b/epilogue-processor/src/main/java/org/wpilib/epilogue/processor/AnnotationProcessor.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashSet; @@ -43,6 +44,8 @@ public class AnnotationProcessor extends AbstractProcessor { private static final String kClassSpecificLoggerFqn = "org.wpilib.epilogue.logging.ClassSpecificLogger"; private static final String kLoggedFqn = "org.wpilib.epilogue.Logged"; + public static final String V3_SCHEDULER_CLASS = "org.wpilib.command3.Scheduler"; + public static final String V2_SCHEDULER_CLASS = "org.wpilib.command2.CommandScheduler"; private EpilogueGenerator m_epilogueGenerator; private LoggerGenerator m_loggerGenerator; @@ -115,13 +118,14 @@ public boolean process(Set annotations, RoundEnvironment m_epilogueGenerator = new EpilogueGenerator(processingEnv, customLoggers); m_loggerGenerator = new LoggerGenerator(processingEnv, m_handlers); + var commandFrameworks = checkLoadedCommandFrameworks(); annotations.stream() .filter(ann -> kLoggedFqn.contentEquals(ann.getQualifiedName())) .findAny() .ifPresent( epilogue -> { - processEpilogue(roundEnv, epilogue, loggedTypes); + processEpilogue(roundEnv, epilogue, loggedTypes, commandFrameworks); }); return false; @@ -377,7 +381,10 @@ private Map processCustomLoggers( } private void processEpilogue( - RoundEnvironment roundEnv, TypeElement epilogueAnnotation, Set loggedTypes) { + RoundEnvironment roundEnv, + TypeElement epilogueAnnotation, + Set loggedTypes, + Collection commandFrameworks) { var annotatedElements = roundEnv.getElementsAnnotatedWith(epilogueAnnotation); List loggerClassNames = new ArrayList<>(); @@ -418,7 +425,7 @@ private void processEpilogue( // Sort alphabetically mainRobotClasses.sort(Comparator.comparing(c -> c.getSimpleName().toString())); - m_epilogueGenerator.writeEpilogueFile(loggerClassNames, mainRobotClasses); + m_epilogueGenerator.writeEpilogueFile(loggerClassNames, mainRobotClasses, commandFrameworks); } private void warnOfNonLoggableElements(TypeElement clazz) { @@ -452,4 +459,19 @@ private void warnOfNonLoggableElements(TypeElement clazz) { } } } + + private Collection checkLoadedCommandFrameworks() { + List commandFrameworks = new ArrayList<>(); + if (processingEnv.getElementUtils().getTypeElement(V3_SCHEDULER_CLASS) != null) { + // Special case: allow the default v3 scheduler to be automatically logged. + // If the project has both v3 and v2 + commandFrameworks.add(CommandFramework.V3); + } else if (processingEnv.getElementUtils().getTypeElement(V2_SCHEDULER_CLASS) != null) { + // Special case: allow the default v2 scheduler to be automatically logged + commandFrameworks.add(CommandFramework.V2); + } else { + // Neither command framework found, no automatic logging. + } + return commandFrameworks; + } } diff --git a/epilogue-processor/src/main/java/org/wpilib/epilogue/processor/CommandFramework.java b/epilogue-processor/src/main/java/org/wpilib/epilogue/processor/CommandFramework.java new file mode 100644 index 00000000000..4c2731129b6 --- /dev/null +++ b/epilogue-processor/src/main/java/org/wpilib/epilogue/processor/CommandFramework.java @@ -0,0 +1,17 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package org.wpilib.epilogue.processor; + +/** + * Represents the different WPILib command frameworks. Epilogue will detect the presence of these + * frameworks at compile time to add special handling, such as automatic logging of the scheduler. + */ +public enum CommandFramework { + /** The Commands V2 framework. */ + V2, + + /** The Commands V3 framework. */ + V3, +} diff --git a/epilogue-processor/src/main/java/org/wpilib/epilogue/processor/EpilogueGenerator.java b/epilogue-processor/src/main/java/org/wpilib/epilogue/processor/EpilogueGenerator.java index ed437431bd6..cc33464d1f1 100644 --- a/epilogue-processor/src/main/java/org/wpilib/epilogue/processor/EpilogueGenerator.java +++ b/epilogue-processor/src/main/java/org/wpilib/epilogue/processor/EpilogueGenerator.java @@ -43,7 +43,9 @@ public EpilogueGenerator( */ @SuppressWarnings("checkstyle:LineLength") // Source code templates exceed the line length limit public void writeEpilogueFile( - List loggerClassNames, Collection mainRobotClasses) { + List loggerClassNames, + Collection mainRobotClasses, + Collection commandFrameworks) { try { var centralStore = m_processingEnv.getFiler().createSourceFile("org.wpilib.epilogue.Epilogue"); @@ -167,6 +169,20 @@ public static boolean shouldLog(Logged.Importance importance) { " " + StringUtils.loggerFieldName(mainRobotClass) + ".tryUpdate(config.backend.getNested(config.root), robot, config.errorHandler);"); + + if (commandFrameworks.contains(CommandFramework.V3)) { + // Special case: automatically log the default commands v3 scheduler object. + // Otherwise, users would need to either manually log it or store it in a logged + // field, which is bad UX. + // Note that the v2 scheduler isn't loggable, so we don't generate anything + out.println( + """ + if (config.automaticallyLogCommandScheduler) { + config.backend.getNested(config.root).log("Command Scheduler", org.wpilib.command3.Scheduler.getDefault(), org.wpilib.command3.Scheduler.proto); + } + """); + } + out.println( " config.backend.log(\"Epilogue/Stats/Last Run\", (System.nanoTime() - start) / 1e6);"); out.println(" }"); diff --git a/epilogue-processor/src/test/java/org/wpilib/epilogue/processor/EpilogueGeneratorTest.java b/epilogue-processor/src/test/java/org/wpilib/epilogue/processor/EpilogueGeneratorTest.java index 4a0395e2854..5d95a9a8cbb 100644 --- a/epilogue-processor/src/test/java/org/wpilib/epilogue/processor/EpilogueGeneratorTest.java +++ b/epilogue-processor/src/test/java/org/wpilib/epilogue/processor/EpilogueGeneratorTest.java @@ -7,6 +7,7 @@ import static com.google.testing.compile.CompilationSubject.assertThat; import static com.google.testing.compile.Compiler.javac; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.wpilib.epilogue.processor.CompileTestOptions.kJavaVersionOptions; import com.google.testing.compile.Compilation; @@ -390,6 +391,79 @@ public static boolean shouldLog(Logged.Importance importance) { assertGeneratedEpilogueContents(source, expected); } + @Test + void commandsv3Scheduler() { + String schedulerSource = + """ + package org.wpilib.command3; + + import org.wpilib.util.protobuf.*; + import us.hebi.quickbuf.*; + + // Stub the scheduler and its protobuf logging so the shape is correct at compile time. + // We don't care about runtime behavior because we are only testing the contents of the + // generated code. + public interface Scheduler extends ProtobufSerializable { + static class ProtoScheduler extends ProtoMessage implements Cloneable { + @Override public ProtoScheduler clone() { return null; } + @Override public boolean equals(Object other) { return false; } + @Override public ProtoScheduler copyFrom(ProtoScheduler other) { return null; } + @Override public ProtoScheduler mergeFrom(ProtoSource other) { return null; } + @Override public ProtoScheduler clear() { return null; } + @Override public int computeSerializedSize() { return 0; } + @Override public void writeTo(ProtoSink output) {} + } + + static Protobuf proto = null; + + static Scheduler getDefault() { + return null; + } + } + """; + + String robotSource = + """ + package org.wpilib.epilogue; + + @Logged + public class Robot extends org.wpilib.framework.TimedRobot {} + """; + + Compilation compilation = + javac() + .withOptions(kJavaVersionOptions) + .withProcessors(new AnnotationProcessor()) + .compile( + JavaFileObjects.forSourceString("org.wpilib.epilogue.Robot", robotSource), + JavaFileObjects.forSourceString("org.wpilib.command3.Scheduler", schedulerSource)); + + assertThat(compilation).succeededWithoutWarnings(); + var generatedFiles = compilation.generatedSourceFiles(); + var epilogueFile = + generatedFiles.stream() + .filter(jfo -> jfo.getName().contains("Epilogue")) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Epilogue file was not generated!")); + + try { + var content = epilogueFile.getCharContent(false); + + assertTrue( + content + .toString() + .contains( + """ + if (config.automaticallyLogCommandScheduler) { + config.backend.getNested(config.root).log("Command Scheduler", org.wpilib.command3.Scheduler.getDefault(), org.wpilib.command3.Scheduler.proto); + } + """), + "Generated file did not contain the expected scheduler logging code:\n\n" + content); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + private void assertGeneratedEpilogueContents( String loggedClassContent, String loggerClassContent) { Compilation compilation = diff --git a/epilogue-runtime/src/main/java/org/wpilib/epilogue/EpilogueConfiguration.java b/epilogue-runtime/src/main/java/org/wpilib/epilogue/EpilogueConfiguration.java index 5e046eb87a8..8f39e2ddc8d 100644 --- a/epilogue-runtime/src/main/java/org/wpilib/epilogue/EpilogueConfiguration.java +++ b/epilogue-runtime/src/main/java/org/wpilib/epilogue/EpilogueConfiguration.java @@ -54,6 +54,16 @@ public class EpilogueConfiguration { */ public String root = "Robot"; + /** + * Whether to automatically log the default commands v3 scheduler. If set to {@code true}, the + * scheduler will be logged under the "Command Scheduler" indentifier in the {@link #root root + * namespace}. If you want to manually log the scheduler with a different identifier, set this to + * {@code false} to avoid duplicating logged data. + * + *

Has no effect if the CommandsV3 vendordep is not present. + */ + public boolean automaticallyLogCommandScheduler = true; + /** Default constructor. */ public EpilogueConfiguration() {} }