diff --git a/lib/src/main/java/com/team2813/lib2813/util/BuildConstants.java b/lib/src/main/java/com/team2813/lib2813/util/BuildConstants.java new file mode 100644 index 00000000..fcd2a2c1 --- /dev/null +++ b/lib/src/main/java/com/team2813/lib2813/util/BuildConstants.java @@ -0,0 +1,16 @@ +package com.team2813.lib2813.util; + +import java.time.ZonedDateTime; + +/** Holder for data collected at build time about the robot code. */ +public interface BuildConstants { + + /** The current git branch when the code was built. */ + String gitBranch(); + + /** The time the most recent commit at HEAD was submitted. */ + ZonedDateTime gitSubmitTime(); + + /** The time the code was built. */ + ZonedDateTime buildTime(); +} diff --git a/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsPublisher.java b/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsPublisher.java index bde76526..7d59e1ca 100644 --- a/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsPublisher.java +++ b/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsPublisher.java @@ -2,6 +2,8 @@ import edu.wpi.first.networktables.NetworkTable; import edu.wpi.first.networktables.NetworkTableInstance; +import java.util.Optional; +import java.util.function.Function; /** * Publishes build constants to NetworkTables. @@ -22,60 +24,25 @@ * *

To instantiate a BuildConstantsPublisher, a build constants class, {@code BuildConstants}, is * needs to be generated for the robot library by enabling the `gversion` plugin in the gradle build - * file. - * - *

{@code
- * plugins {
- *   ...
- *   // Plugin needed for Git Build Info
- *   // (see https://docs.wpilib.org/en/stable/docs/software/advanced-gradlerio/deploy-git-data.html)
- *   id 'com.peterabeles.gversion' version '1.10'
- *   ...
- * }
- * ...
- * // Generates a BuildConstants file.
- * // https://docs.wpilib.org/en/stable/docs/software/advanced-gradlerio/deploy-git-data.html
- * project.compileJava.dependsOn(createVersionFile)
- * def BUILD_CONSTANTS_AUTOGEN_PATH = 'build/generated/sources/build_constants/'
- * gversion {
- *     // Build inside build/ (so that it will be ignored by git due to .gitignore)
- *     // and inside build/generated/ (so that it will be ignored by our Spotless
- *     // rules).
- *     srcDir       = BUILD_CONSTANTS_AUTOGEN_PATH
- *     classPackage = 'com.team2813'
- *     className    = 'BuildConstants'
- *     dateFormat   = 'yyyy-MM-dd HH:mm:ss z'
- *     timeZone     = 'America/Los_Angeles' // Use preferred time zone
- *     indent       = '  '
- * }
- * sourceSets.main.java.srcDirs += BUILD_CONSTANTS_AUTOGEN_PATH
- * ...
- * }
+ * file. Instructions can be found in + * the WPILib documentation. * *

With the BuildConstants generation enabled, the publisher is initialized and used like this: * *

{@code
- * BuildConstantsPublisher buildConstantsPublisher(com.team2813.BuildConstants.class);
+ * BuildConstantsPublisher buildConstantsPublisher(frc.robot.BuildConstants.class);
  * // Publish the build constants to "/Metadata" on the NetworkTables
  * buildConstantsPublisher.publish(NetworkTableInstance.getDefault());
  * // Log the build constants in the robot console as well.
  * buildConstantsPublisher.log();
  * }
*/ -public class BuildConstantsPublisher { +public final class BuildConstantsPublisher { /** The name of the NetworkTable under which the build constants are published. */ public static final String METADATA_TABLE_NAME = "Metadata"; - private String m_mavenName; - // Don't resolve BuildConstants.MAVEN_GROUP because it is always empty - private int m_gitRevision; - // Don't resolve BuildConstants.VERSION because it is always "unspecified". - private String m_gitSha; - private String m_gitDate; - private String m_gitBranch; - private String m_buildDate; - private long m_buildUnixTime; - private int m_dirty; + private final Optional constants; /** * Constructs a BuildConstantsPublisher. @@ -87,20 +54,12 @@ public class BuildConstantsPublisher { * constants. See class description for instructions on how to generate the class. */ public BuildConstantsPublisher(Class buildConstantsClass) { - try { - m_mavenName = (String) buildConstantsClass.getDeclaredField("MAVEN_NAME").get(null); - m_gitRevision = (int) buildConstantsClass.getDeclaredField("GIT_REVISION").get(null); - m_gitSha = (String) buildConstantsClass.getDeclaredField("GIT_SHA").get(null); - m_gitDate = (String) buildConstantsClass.getDeclaredField("GIT_DATE").get(null); - m_gitBranch = (String) buildConstantsClass.getDeclaredField("GIT_BRANCH").get(null); - m_buildDate = (String) buildConstantsClass.getDeclaredField("BUILD_DATE").get(null); - m_buildUnixTime = (long) buildConstantsClass.getDeclaredField("BUILD_UNIX_TIME").get(null); - m_dirty = (int) buildConstantsClass.getDeclaredField("DIRTY").get(null); - } catch (NoSuchFieldException | IllegalAccessException e) { - // TODO(vdikov): Add a proper error logging here so that developers can catch it when it - // happens - e.printStackTrace(); - } + constants = BuildConstantsRecord.fromGeneratedClass(buildConstantsClass); + } + + /** Gets the build constants extracted from the publisher. */ + public Optional buildConstants() { + return constants.map(Function.identity()); } /** @@ -112,26 +71,32 @@ public BuildConstantsPublisher(Class buildConstantsClass) { * constants are published. */ public void publish(NetworkTableInstance ntInstance) { - NetworkTable table = ntInstance.getTable(METADATA_TABLE_NAME); - table.getStringTopic("MavenName").publish().set(m_mavenName); - table.getIntegerTopic("GitRevision").publish().set(m_gitRevision); - table.getStringTopic("GitSha").publish().set(m_gitSha); - table.getStringTopic("GitDate").publish().set(m_gitDate); - table.getStringTopic("GitBranch").publish().set(m_gitBranch); - table.getStringTopic("BuildDate").publish().set(m_buildDate); - table.getIntegerTopic("BuildUnixTime").publish().set(m_buildUnixTime); - table.getIntegerTopic("Dirty").publish().set(m_dirty); + constants.ifPresent( + values -> { + NetworkTable table = ntInstance.getTable(METADATA_TABLE_NAME); + table.getStringTopic("MavenName").publish().set(values.mavenName()); + table.getIntegerTopic("GitRevision").publish().set(values.gitRevision()); + table.getStringTopic("GitSha").publish().set(values.gitSha()); + table.getStringTopic("GitDate").publish().set(values.gitSubmitTimeString()); + table.getStringTopic("GitBranch").publish().set(values.gitBranch()); + table.getStringTopic("BuildDate").publish().set(values.buildTimeString()); + table.getIntegerTopic("BuildUnixTime").publish().set(values.buildTimeMillis()); + table.getIntegerTopic("Dirty").publish().set(values.dirty()); + }); } /** Logs the build constants to the console. */ public void log() { - System.out.println("MavenName: " + m_mavenName); - System.out.println("GitRevision: " + m_gitRevision); - System.out.println("GitSha: " + m_gitSha); - System.out.println("GitDate: " + m_gitDate); - System.out.println("GitBranch: " + m_gitBranch); - System.out.println("BuildDate: " + m_buildDate); - System.out.println("BuildUnixTime: " + m_buildUnixTime); - System.out.println("Dirty: " + m_dirty); + constants.ifPresent( + values -> { + System.out.println("MavenName: " + values.mavenName()); + System.out.println("GitRevision: " + values.gitRevision()); + System.out.println("GitSha: " + values.gitSha()); + System.out.println("GitDate: " + values.gitSubmitTimeString()); + System.out.println("GitBranch: " + values.gitBranch()); + System.out.println("BuildDate: " + values.buildTimeString()); + System.out.println("BuildUnixTime: " + values.buildTimeMillis()); + System.out.println("Dirty: " + values.dirty()); + }); } } diff --git a/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsRecord.java b/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsRecord.java new file mode 100644 index 00000000..56b2a6d5 --- /dev/null +++ b/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsRecord.java @@ -0,0 +1,95 @@ +package com.team2813.lib2813.util; + +import edu.wpi.first.wpilibj.DriverStation; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Optional; + +record BuildConstantsRecord( + String mavenName, + int gitRevision, + String gitSha, + String gitBranch, + ZonedDateTime gitSubmitTime, + ZonedDateTime buildTime, + long buildTimeMillis, + int dirty) + implements BuildConstants { + private static final DateTimeFormatter DATE_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z"); + + /** + * Constructs an instance from a class generated by the gversion Gradle plugin. + * + *

Instructions for using the gversion Gradle plugin can be found buildConstantsClass, String fieldName) + throws NoSuchFieldException, IllegalAccessException { + String value = (String) buildConstantsClass.getDeclaredField(fieldName).get(null); + try { + return ZonedDateTime.parse(value, DATE_FORMATTER); + } catch (DateTimeParseException e) { + String message = + "Could not extract build constants from " + + buildConstantsClass.getSimpleName() + + " due to unparsable date-time value for " + + fieldName + + ": " + + e.getMessage(); + DriverStation.reportWarning(message, e.getStackTrace()); + return null; + } + } +} diff --git a/lib/src/test/java/com/team2813/lib2813/util/BuildConstantsPublisherTest.java b/lib/src/test/java/com/team2813/lib2813/util/BuildConstantsPublisherTest.java index a105e8b8..89ddce28 100644 --- a/lib/src/test/java/com/team2813/lib2813/util/BuildConstantsPublisherTest.java +++ b/lib/src/test/java/com/team2813/lib2813/util/BuildConstantsPublisherTest.java @@ -11,12 +11,19 @@ import edu.wpi.first.networktables.NetworkTableInstance; import java.io.ByteArrayOutputStream; import java.io.PrintStream; +import java.time.Instant; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import org.junit.jupiter.api.Test; public class BuildConstantsPublisherTest { + // This format must be consistent with the `createVersionFile` settings in the build.gradle. + private static final DateTimeFormatter DATE_TIME_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z"); + // Fake constants copied and adapted from this article // https://docs.wpilib.org/en/stable/docs/software/advanced-gradlerio/deploy-git-data.html public final class FakeBuildConstants { @@ -29,7 +36,7 @@ public final class FakeBuildConstants { public static final String GIT_BRANCH = "main"; public static final String BUILD_DATE = "2023-10-27 12:29:57 EDT"; public static final long BUILD_UNIX_TIME = 1698424197122L; - public static final int DIRTY = 0; + public static final int DIRTY = 1; private FakeBuildConstants() {} } @@ -42,9 +49,6 @@ private FakeBuildConstants() {} */ private class DateTimeStringSubject extends Subject { private final String actual; - // The format must be consistent with the `createVersionFile` settings in the build.gradle. - private final DateTimeFormatter formatter = - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z"); private DateTimeStringSubject(FailureMetadata metadata, String actual) { super(metadata, actual); @@ -58,10 +62,10 @@ public void parsesAsLocalDateTime() { } try { - LocalDateTime.parse(actual, formatter); + LocalDateTime.parse(actual, DATE_TIME_FORMATTER); } catch (DateTimeParseException e) { failWithActual( - fact("expected to parse as LocalDateTime with format", formatter), + fact("expected to parse as LocalDateTime with format", DATE_TIME_FORMATTER), fact("but parsing failed with", e.getMessage())); } } @@ -83,6 +87,36 @@ private Long getIntegerEntryOrDefault(NetworkTable table, String key, long defau return table.getIntegerTopic(key).getEntry(defaultValue).get(); } + @Test + public void extractsBuildConstants() { + // Arrange. + BuildConstantsPublisher publisher = new BuildConstantsPublisher(FakeBuildConstants.class); + + // Act. + var constants = publisher.buildConstants(); + + // Assert. + ZonedDateTime expectedBuildTime = + ZonedDateTime.ofInstant( + Instant.ofEpochMilli(FakeBuildConstants.BUILD_UNIX_TIME), + ZoneId.of("America/New_York")) + .withNano(0); + ZonedDateTime expectedGitCommitTime = + ZonedDateTime.parse(FakeBuildConstants.GIT_DATE, DATE_TIME_FORMATTER); + var expectedRecord = + new BuildConstantsRecord( + FakeBuildConstants.MAVEN_NAME, + FakeBuildConstants.GIT_REVISION, + FakeBuildConstants.GIT_SHA, + FakeBuildConstants.GIT_BRANCH, + expectedGitCommitTime, + expectedBuildTime, + FakeBuildConstants.BUILD_UNIX_TIME, + FakeBuildConstants.DIRTY); + + assertThat(constants).hasValue(expectedRecord); + } + @Test public void publishesBuildConstantsToNetworkTables() { // Arrange. @@ -117,7 +151,9 @@ public void publishesBuildConstantsToNetworkTables() { assertAbout(DateTimeStringSubject::new) .that(getStringEntryOrEmpty(table, "BuildDate")) .parsesAsLocalDateTime(); - assertThat(getIntegerEntryOrDefault(table, "Dirty", -1)).isAnyOf(0l, 1l); + assertThat(getIntegerEntryOrDefault(table, "Dirty", -1)).isAnyOf(0L, 1L); + + ntInstance.close(); } @Test