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
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <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>
*
* <p>With the BuildConstants generation enabled, the publisher is initialized and used like this:
*
* <pre>{@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();
* }</pre>
*/
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.
*
* <p>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.
*
* <p>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);
}
}
Original file line number Diff line number Diff line change
@@ -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}.
*
* <p>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.METADATA_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);
}
}
}
Loading