Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions lib/src/main/java/com/team2813/lib2813/util/BuildConstants.java
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -22,60 +24,25 @@
*
* <p>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.
*
* <pre>{@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
* ...
* }</pre>
* file. Instructions can be found <a
* href="https://docs.wpilib.org/en/stable/docs/software/advanced-gradlerio/deploy-git-data.html">in
* the WPILib documentation</a>.
*
* <p>With the BuildConstants generation enabled, the publisher is initialized and used like this:
*
* <pre>{@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();
* }</pre>
*/
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<BuildConstantsRecord> constants;

/**
* Constructs a BuildConstantsPublisher.
Expand All @@ -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> buildConstants() {
return constants.map(Function.identity());
}

/**
Expand All @@ -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());
});
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Instructions for using the gversion Gradle plugin can be found <a
* href="https://docs.wpilib.org/en/stable/docs/software/advanced-gradlerio/deploy-git-data.html>here</a>.
*
* @param buildConstantsClass Specially built class that contains the robot code built-time
* constants.
*/
static Optional<BuildConstantsRecord> fromGeneratedClass(Class<?> buildConstantsClass) {
try {
ZonedDateTime gitDate = extractZonedDateTime(buildConstantsClass, "GIT_DATE");
if (gitDate == null) {
return Optional.empty();
}
ZonedDateTime buildDate = extractZonedDateTime(buildConstantsClass, "BUILD_DATE");
if (buildDate == null) {
return Optional.empty();
}
String mavenName = (String) buildConstantsClass.getDeclaredField("MAVEN_NAME").get(null);
int gitRevision = (int) buildConstantsClass.getDeclaredField("GIT_REVISION").get(null);
String gitSha = (String) buildConstantsClass.getDeclaredField("GIT_SHA").get(null);
String gitBranch = (String) buildConstantsClass.getDeclaredField("GIT_BRANCH").get(null);
long buildTimeMillis =
(long) buildConstantsClass.getDeclaredField("BUILD_UNIX_TIME").get(null);
int dirty = (int) buildConstantsClass.getDeclaredField("DIRTY").get(null);

return Optional.of(
new BuildConstantsRecord(
mavenName,
gitRevision,
gitSha,
gitBranch,
gitDate,
buildDate,
buildTimeMillis,
dirty));
} catch (NoSuchFieldException | IllegalAccessException e) {
String message =
"Could not extract build constants from "
+ buildConstantsClass.getSimpleName()
+ ": "
+ e.getMessage();
DriverStation.reportWarning(message, e.getStackTrace());
}
return Optional.empty();
}

String gitSubmitTimeString() {
return DATE_FORMATTER.format(gitSubmitTime);
}

String buildTimeString() {
return DATE_FORMATTER.format(buildTime);
}

private static ZonedDateTime extractZonedDateTime(Class<?> 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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() {}
}
Expand All @@ -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);
Expand All @@ -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()));
}
}
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down
Loading