From f52a2c7411e3ba455d925e4d3aff2921f6970423 Mon Sep 17 00:00:00 2001 From: Veselin Dikov Date: Sat, 6 Sep 2025 17:02:32 -0700 Subject: [PATCH 1/2] Port BuildConstantsPublisher from /Robot2025 code to lib2813. Make the class work with BuildConstants provided at initialization, not hard-coded, so we can use from different robot code repositories --- .../lib2813/util/BuildConstantsPublisher.java | 137 +++++++++++++++ .../util/BuildConstantsPublisherTest.java | 165 ++++++++++++++++++ 2 files changed, 302 insertions(+) create mode 100644 lib/src/main/java/com/team2813/lib2813/util/BuildConstantsPublisher.java create mode 100644 lib/src/test/java/com/team2813/lib2813/util/BuildConstantsPublisherTest.java diff --git a/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsPublisher.java b/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsPublisher.java new file mode 100644 index 00000000..bde76526 --- /dev/null +++ b/lib/src/main/java/com/team2813/lib2813/util/BuildConstantsPublisher.java @@ -0,0 +1,137 @@ +package com.team2813.lib2813.util; + +import edu.wpi.first.networktables.NetworkTable; +import edu.wpi.first.networktables.NetworkTableInstance; + +/** + * Publishes build constants to NetworkTables. + * + *

The "build constants" are metadata related to the state of the code at the time it was built, + * e.g, git branch, git commit, build time, etc. This information can be very valuable when + * troubleshooting issues with the live code of the robot. + * + *

BuildConstantsPublisher receives this information from a specially built (as explained below) + * BuildConstants class at build time. It provides an interface to publish it to NetworkTables + * ({@link #publish(NetworkTableInstance)}) or print it to the robot console ({@link #log()}) - at + * runtime. Build constants need to be published only once, typically during robot initialization. + * + *

The constants are published under the {@code "/Metadata"} table in NetworkTables. This is a + * special NetworkTables table. Some tools have special support for the "/Metadata" table. For + * instance, Advantage Scope has a dedicated Metadata tab that loads information like Build + * Constants in a well formated table view. + * + *

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
+ * ...
+ * }
+ * + *

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

{@code
+ * BuildConstantsPublisher buildConstantsPublisher(com.team2813.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 { + /** 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; + + /** + * Constructs a BuildConstantsPublisher. + * + *

This constructor creates publishers for each build constant and publishes them to the + * provided network table instance. + * + * @param buildConstantsClass Specially built class that contains the robot code built-time + * 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(); + } + } + + /** + * Publishes the build constants to NetworkTables. + * + *

This is typically called once during robot initialization. + * + * @param ntInstance The top-level NetworkTable instance under whose "/Metadata" table the build + * 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); + } + + /** 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); + } +} diff --git a/lib/src/test/java/com/team2813/lib2813/util/BuildConstantsPublisherTest.java b/lib/src/test/java/com/team2813/lib2813/util/BuildConstantsPublisherTest.java new file mode 100644 index 00000000..3c06d57d --- /dev/null +++ b/lib/src/test/java/com/team2813/lib2813/util/BuildConstantsPublisherTest.java @@ -0,0 +1,165 @@ +package com.team2813.lib2813.util; + +import static com.google.common.truth.Fact.fact; +import static com.google.common.truth.Fact.simpleFact; +import static com.google.common.truth.Truth.assertAbout; +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.truth.FailureMetadata; +import com.google.common.truth.Subject; +import edu.wpi.first.networktables.NetworkTable; +import edu.wpi.first.networktables.NetworkTableInstance; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import org.junit.jupiter.api.Test; + +public class BuildConstantsPublisherTest { + // 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 { + public static final String MAVEN_GROUP = ""; + public static final String MAVEN_NAME = "2813Robot"; + public static final String VERSION = "unspecified"; + public static final int GIT_REVISION = 1; + public static final String GIT_SHA = "fad108a4b1c1dcdfc8859c6295ea64e06d43f557"; + public static final String GIT_DATE = "2023-10-26 17:38:59 EDT"; + 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; + + private FakeBuildConstants() {} + } + + /** + * A Truth {@link Subject} for asserting properties of strings that should parse as {@link + * LocalDateTime}. + * + *

Composed with the help of Gemini: https://g.co/gemini/share/d8db68a8fbaf + */ + 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); + this.actual = actual; + } + + public void parsesAsLocalDateTime() { + if (actual == null) { + failWithActual(simpleFact("expected to parse as LocalDateTime, but was null")); + return; + } + + try { + LocalDateTime.parse(actual, formatter); + } catch (DateTimeParseException e) { + failWithActual( + fact("expected to parse as LocalDateTime with format", formatter), + fact("but parsing failed with", e.getMessage())); + } + } + } + + /** + * Returns the value of the given key in the given table, or an empty string if the key is not + * present. + */ + private String getStringEntryOrEmpty(NetworkTable table, String key) { + return table.getStringTopic(key).getEntry("").get(); + } + + /** + * Returns the value of the given key in the given table, or the given default value if the key is + * not present. + */ + private Long getIntegerEntryOrDefault(NetworkTable table, String key, long defaultValue) { + return table.getIntegerTopic(key).getEntry(defaultValue).get(); + } + + @Test + public void publishesBuildConstantsToNetworkTables() { + // Arrange. + NetworkTableInstance ntInstance = NetworkTableInstance.create(); + BuildConstantsPublisher publisher = new BuildConstantsPublisher(FakeBuildConstants.class); + NetworkTable table = ntInstance.getTable(BuildConstantsPublisher.TABLE_NAME); + + // Act. + publisher.publish(ntInstance); + + // Assert. + assertThat(table).isNotNull(); + assertThat(table.getKeys()) + .containsExactly( + "MavenName", + "GitRevision", + "GitSha", + "GitDate", + "GitBranch", + "BuildUnixTime", + "BuildDate", + "Dirty"); + assertThat(getStringEntryOrEmpty(table, "MavenName")).isEqualTo("2813Robot"); + + assertThat(getIntegerEntryOrDefault(table, "GitRevision", 0)).isGreaterThan(0); + assertThat(getStringEntryOrEmpty(table, "GitSha")).isNotEmpty(); + assertThat(getStringEntryOrEmpty(table, "GitDate")).isNotEmpty(); + assertThat(getStringEntryOrEmpty(table, "GitBranch")).isNotEmpty(); + + assertThat(getIntegerEntryOrDefault(table, "BuildUnixTime", 0)).isNotEqualTo(0); + assertThat(getStringEntryOrEmpty(table, "BuildDate")).isNotEmpty(); + assertAbout(DateTimeStringSubject::new) + .that(getStringEntryOrEmpty(table, "BuildDate")) + .parsesAsLocalDateTime(); + assertThat(getIntegerEntryOrDefault(table, "Dirty", -1)).isAnyOf(0l, 1l); + } + + @Test + public void logsBuildConstantsToConsole() { + // Arrange. + + BuildConstantsPublisher publisher = new BuildConstantsPublisher(FakeBuildConstants.class); + + // Keep the original System.out + PrintStream originalOut = System.out; + + // Redirect System.out to a ByteArrayOutputStream + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + + // Act. + publisher.log(); + + // Assert. + try { + assertThat(outputStream.toString()) + .containsMatch( + // NOTE that \r?\n is used to match both Windows (\r\n) and Unix (\n) line endings. + "MavenName: 2813Robot\r?\n" + // Matches a Git revision number, e.g., "121" + + "GitRevision: [0-9]+\r?\n" + // Matches a Git revision hash, e.g., "08205a25fe10c6c6c1ea4db2deabb4aaf4617637" + // Accepts "NA" for users that have no git installed. + + "GitSha: (NA|[0-9a-f]{40})\r?\n" + // Matches a Git date, e.g., "2023-10-01 12:34:56 PDT" + + "GitDate: \\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.+\r?\n" + // Matches a Git branch name, e.g., "main" + + "GitBranch: .+\r?\n" + // Matches a build date, e.g., "2023-10-01 12:34:56 PDT" + + "BuildDate: \\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.+\r?\n" + // Matches a Unix timestamp, e.g., "1696175696" + + "BuildUnixTime: \\d+\r?\n" + // Matches a dirty flag, e.g., "0" or "1" + + "Dirty: [01]\r?\n"); + } finally { + // Restore System.out + System.setOut(originalOut); + } + } +} From 3a669481c6dfaa1d9c523ff3e9966bbee3fb1a6e Mon Sep 17 00:00:00 2001 From: Veselin Dikov Date: Sat, 6 Sep 2025 17:18:32 -0700 Subject: [PATCH 2/2] Fix compile error --- .../com/team2813/lib2813/util/BuildConstantsPublisherTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3c06d57d..a105e8b8 100644 --- a/lib/src/test/java/com/team2813/lib2813/util/BuildConstantsPublisherTest.java +++ b/lib/src/test/java/com/team2813/lib2813/util/BuildConstantsPublisherTest.java @@ -88,7 +88,7 @@ public void publishesBuildConstantsToNetworkTables() { // Arrange. NetworkTableInstance ntInstance = NetworkTableInstance.create(); BuildConstantsPublisher publisher = new BuildConstantsPublisher(FakeBuildConstants.class); - NetworkTable table = ntInstance.getTable(BuildConstantsPublisher.TABLE_NAME); + NetworkTable table = ntInstance.getTable(BuildConstantsPublisher.METADATA_TABLE_NAME); // Act. publisher.publish(ntInstance);