From c527bf4583f5c24657c07db6659a53a1242d0a95 Mon Sep 17 00:00:00 2001 From: Kevin Cooney Date: Sat, 23 Aug 2025 14:32:13 -0700 Subject: [PATCH 1/2] Update WPILibExtension to fail test if HAL.initialize() returns false --- .../lib2813/testing/junit/jupiter/WPILibExtension.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/testing/src/main/java/com/team2813/lib2813/testing/junit/jupiter/WPILibExtension.java b/testing/src/main/java/com/team2813/lib2813/testing/junit/jupiter/WPILibExtension.java index d6787beb..262b70a5 100644 --- a/testing/src/main/java/com/team2813/lib2813/testing/junit/jupiter/WPILibExtension.java +++ b/testing/src/main/java/com/team2813/lib2813/testing/junit/jupiter/WPILibExtension.java @@ -52,7 +52,9 @@ public final class WPILibExtension @Override public void beforeAll(ExtensionContext context) { // See https://www.chiefdelphi.com/t/driverstation-getalliance-in-gradle-test/ - HAL.initialize(500, 0); + if (!HAL.initialize(500, 0)) { + throw new IllegalStateException("Could not initialize Hardware Abstraction Layer"); + } DriverStationSim.setEnabled(true); DriverStationSim.notifyNewData(); CommandScheduler.getInstance().enable(); From b32a738f23633019e064d94f85e6fe48bef57aa9 Mon Sep 17 00:00:00 2001 From: Kevin Cooney Date: Tue, 26 Aug 2025 22:55:57 -0700 Subject: [PATCH 2/2] Add tests for WPILibExtension --- testing/build.gradle | 5 +- .../junit/jupiter/WPILibExtensionTest.java | 175 ++++++++++++++++++ 2 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 testing/src/test/java/com/team2813/lib2813/testing/junit/jupiter/WPILibExtensionTest.java diff --git a/testing/build.gradle b/testing/build.gradle index afb273a6..ecd2e849 100644 --- a/testing/build.gradle +++ b/testing/build.gradle @@ -29,6 +29,7 @@ dependencies { nativeRelease wpi.java.vendor.jniRelease(wpi.platforms.desktop) simulationRelease wpi.sim.enableRelease() + testImplementation('org.junit.platform:junit-platform-testkit') testRuntimeOnly('org.junit.platform:junit-platform-launcher') } @@ -37,7 +38,9 @@ wpi.java.configureTestTasks(test) tasks.named('test') { // Support running both JUnit Vintage and JUnit Jupiter tests - useJUnitPlatform() + useJUnitPlatform { + excludeTags('ignore-outside-testkit') + } systemProperty 'junit.jupiter.extensions.autodetection.enabled', 'true' } diff --git a/testing/src/test/java/com/team2813/lib2813/testing/junit/jupiter/WPILibExtensionTest.java b/testing/src/test/java/com/team2813/lib2813/testing/junit/jupiter/WPILibExtensionTest.java new file mode 100644 index 00000000..3fd1cfe5 --- /dev/null +++ b/testing/src/test/java/com/team2813/lib2813/testing/junit/jupiter/WPILibExtensionTest.java @@ -0,0 +1,175 @@ +package com.team2813.lib2813.testing.junit.jupiter; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +import edu.wpi.first.hal.HAL; +import edu.wpi.first.wpilibj.DriverStation; +import edu.wpi.first.wpilibj.RobotState; +import edu.wpi.first.wpilibj.simulation.DriverStationSim; +import edu.wpi.first.wpilibj2.command.Command; +import edu.wpi.first.wpilibj2.command.CommandScheduler; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.platform.testkit.engine.EngineExecutionResults; +import org.junit.platform.testkit.engine.EngineTestKit; +import org.junit.platform.testkit.engine.Events; + +/** Tests for {@link WPILibExtension}. */ +public class WPILibExtensionTest { + + static final Command FAKE_COMMAND = new Command() {}; + + @BeforeAll + static void initializeHal() { + if (!HAL.initialize(500, 0)) { + throw new IllegalStateException("Could not initialize Hardware Abstraction Layer"); + } + } + + @ExtendWith(WPILibExtension.class) + @Tag("ignore-outside-testkit") + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + public static class SampleTest { + + @BeforeAll + public static void verifyFakeCommandNotScheduledBeforeAll() { + CommandScheduler commandScheduler = CommandScheduler.getInstance(); + assertWithMessage("Expect all commands to have been cancelled") + .that(commandScheduler.isScheduled(FAKE_COMMAND)) + .isFalse(); + } + + @BeforeAll + public static void verifyDriverStationEnabled() { + assertThat(DriverStation.isEnabled()).isTrue(); + } + + @Test + @Order(1) + public void verifyFakeCommandNotScheduledBeforeTest() { + CommandScheduler commandScheduler = CommandScheduler.getInstance(); + assertWithMessage("Expect all commands to have been cancelled") + .that(commandScheduler.isScheduled(FAKE_COMMAND)) + .isFalse(); + + commandScheduler.schedule(FAKE_COMMAND); + assertThat(commandScheduler.isScheduled(FAKE_COMMAND)); + } + + @Test + @Order(2) + public void verifyFakeCommandNotScheduledAfterTest(CommandTester commandTester) { + CommandScheduler commandScheduler = CommandScheduler.getInstance(); + assertWithMessage("Expect all commands to have been cancelled") + .that(commandScheduler.isScheduled(FAKE_COMMAND)) + .isFalse(); + + commandScheduler.schedule(FAKE_COMMAND); + assertThat(commandScheduler.isScheduled(FAKE_COMMAND)); + } + + @Test + @Order(3) + public void verifyCommandTester(CommandTester commandTester) { + VerifiableCommand command = new VerifiableCommand(); + commandTester.runUntilComplete(command); + command.verify(); + } + + @AfterAll + public static void verifyFakeCommandNotScheduledAfterAllTests() { + CommandScheduler commandScheduler = CommandScheduler.getInstance(); + assertWithMessage("Expect all commands to have been cancelled") + .that(commandScheduler.isScheduled(FAKE_COMMAND)) + .isFalse(); + } + } // end SampleTest + + @Test + void verifyExtension() { + // Arrange + withDriverStationTemporarilyEnabled( + () -> { + // Schedule FAKE_COMMAND + CommandScheduler commandScheduler = CommandScheduler.getInstance(); + commandScheduler.enable(); + commandScheduler.schedule(FAKE_COMMAND); + boolean isScheduled = commandScheduler.isScheduled(FAKE_COMMAND); + commandScheduler.disable(); + assertThat(isScheduled).isTrue(); + }); + + // Act + EngineExecutionResults results = + EngineTestKit.engine("junit-jupiter").selectors(selectClass(SampleTest.class)).execute(); + + // Assert + assertHasNoFailures(results); + } + + private void withDriverStationTemporarilyEnabled(Runnable runnable) { + assertThat(RobotState.isDisabled()).isTrue(); + DriverStationSim.setEnabled(true); + DriverStationSim.notifyNewData(); + assertThat(RobotState.isDisabled()).isFalse(); + + try { + runnable.run(); + } finally { + DriverStationSim.setEnabled(false); + DriverStationSim.notifyNewData(); + } + } + + private void assertHasNoFailures(EngineExecutionResults results) { + assertHasNoFailures(results.containerEvents()); + assertHasNoFailures(results.testEvents()); + } + + private void assertHasNoFailures(Events events) { + events.assertStatistics( + stats -> { + stats.skipped(0); + stats.failed(0); + }); + } + + private static class VerifiableCommand extends Command { + private static final int EXPECTED_EXECUTION_COUNT = 4; + private int initializedCount = 0; + private int executionCount = 0; + + void verify() { + assertWithMessage("initialize() should be called").that(initializedCount).isGreaterThan(0); + assertWithMessage("initialize() should not be called more than once") + .that(initializedCount) + .isLessThan(2); + assertWithMessage("execute() should be called until isFinished() returns false") + .that(executionCount) + .isEqualTo(EXPECTED_EXECUTION_COUNT); + } + + @Override + public void initialize() { + initializedCount++; + } + + @Override + public void execute() { + executionCount++; + } + + @Override + public boolean isFinished() { + return executionCount >= EXPECTED_EXECUTION_COUNT; + } + } +}