From 7ec15131b559576b0a4e5b06d8f53818978b7981 Mon Sep 17 00:00:00 2001 From: Kevin Cooney Date: Sun, 1 Feb 2026 11:29:29 -0800 Subject: [PATCH 1/3] Simplify creation of Namespace Create `Namespace1 only from the extension class name (the method name is part of the `ExtensionContext` when `resolveParameter()` is called for test method parameter injection) --- .../junit/jupiter/IsolatedNetworkTablesExtension.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/testing/src/main/java/com/team2813/lib2813/testing/junit/jupiter/IsolatedNetworkTablesExtension.java b/testing/src/main/java/com/team2813/lib2813/testing/junit/jupiter/IsolatedNetworkTablesExtension.java index eb82b005..9c82bd24 100644 --- a/testing/src/main/java/com/team2813/lib2813/testing/junit/jupiter/IsolatedNetworkTablesExtension.java +++ b/testing/src/main/java/com/team2813/lib2813/testing/junit/jupiter/IsolatedNetworkTablesExtension.java @@ -46,13 +46,14 @@ */ public final class IsolatedNetworkTablesExtension implements Extension, AfterEachCallback, ParameterResolver { + private static final Namespace NAMESPACE = Namespace.create(IsolatedNetworkTablesExtension.class); private static final StoreKey NETWORK_TABLE_INSTANCE_KEY = StoreKey.of(NetworkTableInstance.class); @Override public void afterEach(ExtensionContext context) { // If this extension created a temporary NetworkTableInstance, close it. - var ntInstance = NETWORK_TABLE_INSTANCE_KEY.remove(getStore(context)); + var ntInstance = NETWORK_TABLE_INSTANCE_KEY.remove(context.getStore(NAMESPACE)); if (ntInstance != null) { // Clear out the listener queue before destroying our temporary NetworkTableInstance. // @@ -81,7 +82,7 @@ public boolean supportsParameter( public NetworkTableInstance resolveParameter( ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { - Store store = getStore(extensionContext); + Store store = extensionContext.getStore(NAMESPACE); NetworkTableInstance ntInstance = NETWORK_TABLE_INSTANCE_KEY.getOrComputeIfAbsent(store, NetworkTableInstance::create); @@ -89,9 +90,4 @@ public NetworkTableInstance resolveParameter( Preferences.setNetworkTableInstance(ntInstance); return ntInstance; } - - /** Gets the {@link Store} for this extension. */ - private Store getStore(ExtensionContext context) { - return context.getStore(Namespace.create(getClass(), context.getRequiredTestMethod())); - } } From 0c476cefe9a6db0ffcd898ac235b6d3e169249a4 Mon Sep 17 00:00:00 2001 From: Kevin Cooney Date: Sun, 1 Feb 2026 11:49:25 -0800 Subject: [PATCH 2/3] Install test NetworkTableInstance in beforeEach() This ensures that tests that modify Preferences use a test instance even if they do not inject a NetworkTableInstance into the test method. --- .../IsolatedNetworkTablesExtension.java | 44 +++++++++++++------ .../IsolatedNetworkTablesExtensionTest.java | 9 +++- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/testing/src/main/java/com/team2813/lib2813/testing/junit/jupiter/IsolatedNetworkTablesExtension.java b/testing/src/main/java/com/team2813/lib2813/testing/junit/jupiter/IsolatedNetworkTablesExtension.java index 9c82bd24..f6efd6ee 100644 --- a/testing/src/main/java/com/team2813/lib2813/testing/junit/jupiter/IsolatedNetworkTablesExtension.java +++ b/testing/src/main/java/com/team2813/lib2813/testing/junit/jupiter/IsolatedNetworkTablesExtension.java @@ -18,6 +18,7 @@ import edu.wpi.first.networktables.NetworkTableInstance; import edu.wpi.first.wpilibj.Preferences; import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext.Namespace; @@ -45,29 +46,37 @@ * @since 2.0.0 */ public final class IsolatedNetworkTablesExtension - implements Extension, AfterEachCallback, ParameterResolver { + implements Extension, BeforeEachCallback, AfterEachCallback, ParameterResolver { private static final Namespace NAMESPACE = Namespace.create(IsolatedNetworkTablesExtension.class); - private static final StoreKey NETWORK_TABLE_INSTANCE_KEY = - StoreKey.of(NetworkTableInstance.class); + private static final StoreKey DATA_KEY = StoreKey.of(Data.class); + + @Override + public void beforeEach(ExtensionContext context) { + Store store = context.getStore(NAMESPACE); + NetworkTableInstance ntInstance = + DATA_KEY.getOrComputeIfAbsent(store, Data::create).testInstance; + Preferences.setNetworkTableInstance(ntInstance); + } @Override public void afterEach(ExtensionContext context) { // If this extension created a temporary NetworkTableInstance, close it. - var ntInstance = NETWORK_TABLE_INSTANCE_KEY.remove(context.getStore(NAMESPACE)); - if (ntInstance != null) { + Store store = context.getStore(NAMESPACE); + Data data = DATA_KEY.remove(store); + if (data != null) { // Clear out the listener queue before destroying our temporary NetworkTableInstance. // // This works around a race condition in WPILib where a listener registered by Preferences can // be called after the NetworkTableInstance was closed (see // https://github.com/wpilibsuite/allwpilib/issues/8215). - if (!ntInstance.waitForListenerQueue(.4)) { + if (!data.testInstance.waitForListenerQueue(.5)) { System.err.println( - "Timed out waiting for the NetworkTableInstance listener queue to empty (waited 200ms);" + "Timed out waiting for the NetworkTableInstance listener queue to empty (waited 500ms);" + " JVM may crash"); } - Preferences.setNetworkTableInstance(NetworkTableInstance.getDefault()); - ntInstance.close(); + Preferences.setNetworkTableInstance(data.prevInstance); + data.testInstance.close(); } } @@ -75,6 +84,9 @@ public void afterEach(ExtensionContext context) { public boolean supportsParameter( ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + if (parameterContext.getTarget().isEmpty()) { + return false; // The extension only supports test method parameter injection. + } return NetworkTableInstance.class.equals(parameterContext.getParameter().getType()); } @@ -83,11 +95,15 @@ public NetworkTableInstance resolveParameter( ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { Store store = extensionContext.getStore(NAMESPACE); - NetworkTableInstance ntInstance = - NETWORK_TABLE_INSTANCE_KEY.getOrComputeIfAbsent(store, NetworkTableInstance::create); + return DATA_KEY.get(store).testInstance; + } - ntInstance.startLocal(); - Preferences.setNetworkTableInstance(ntInstance); - return ntInstance; + private record Data(NetworkTableInstance testInstance, NetworkTableInstance prevInstance) { + static Data create() { + NetworkTableInstance testInstance = NetworkTableInstance.create(); + testInstance.startLocal(); + NetworkTableInstance prevInstance = Preferences.getNetworkTable().getInstance(); + return new Data(testInstance, prevInstance); + } } } diff --git a/testing/src/test/java/com/team2813/lib2813/testing/junit/jupiter/IsolatedNetworkTablesExtensionTest.java b/testing/src/test/java/com/team2813/lib2813/testing/junit/jupiter/IsolatedNetworkTablesExtensionTest.java index ef6c25d5..cc0e0665 100644 --- a/testing/src/test/java/com/team2813/lib2813/testing/junit/jupiter/IsolatedNetworkTablesExtensionTest.java +++ b/testing/src/test/java/com/team2813/lib2813/testing/junit/jupiter/IsolatedNetworkTablesExtensionTest.java @@ -1,5 +1,5 @@ /* -Copyright 2025 Prospect Robotics SWENext Club +Copyright 2025-2026 Prospect Robotics SWENext Club Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import edu.wpi.first.networktables.NetworkTableInstance; +import edu.wpi.first.wpilibj.Preferences; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -42,6 +43,12 @@ public void verifyProvidesNetworkTableParameter(NetworkTableInstance ntInstance) assertThat(ntInstance.getHandle()) .isNotEqualTo(NetworkTableInstance.getDefault().getHandle()); } + + @Test + public void verifyReplacesPreferencesNetworkTableInstance() { + assertThat(Preferences.getNetworkTable().getInstance().getHandle()) + .isNotEqualTo(NetworkTableInstance.getDefault().getHandle()); + } } // end SampleTest @Test From b2aeeff53b787456db4a3a1f4bd267b209088470 Mon Sep 17 00:00:00 2001 From: Kevin Cooney Date: Sun, 1 Feb 2026 09:28:15 -0800 Subject: [PATCH 3/3] Restore Preferences NetworkTableInstance before calling waitForListenerQueue() --- .../team2813/lib2813/preferences/IsolatedPreferences.java | 4 ++-- .../junit/jupiter/IsolatedNetworkTablesExtension.java | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/src/test/java/com/team2813/lib2813/preferences/IsolatedPreferences.java b/lib/src/test/java/com/team2813/lib2813/preferences/IsolatedPreferences.java index 0dc32574..b9be8530 100644 --- a/lib/src/test/java/com/team2813/lib2813/preferences/IsolatedPreferences.java +++ b/lib/src/test/java/com/team2813/lib2813/preferences/IsolatedPreferences.java @@ -42,12 +42,12 @@ protected void before() { @Override protected void after() { - if (!tempInstance.waitForListenerQueue(.4)) { + Preferences.setNetworkTableInstance(NetworkTableInstance.getDefault()); + if (!tempInstance.waitForListenerQueue(.2)) { System.err.println( "Timed out waiting for the NetworkTableInstance listener queue to empty (waited 200ms);" + " JVM may crash"); } - Preferences.setNetworkTableInstance(NetworkTableInstance.getDefault()); tempInstance.close(); } } diff --git a/testing/src/main/java/com/team2813/lib2813/testing/junit/jupiter/IsolatedNetworkTablesExtension.java b/testing/src/main/java/com/team2813/lib2813/testing/junit/jupiter/IsolatedNetworkTablesExtension.java index f6efd6ee..4b90a8a3 100644 --- a/testing/src/main/java/com/team2813/lib2813/testing/junit/jupiter/IsolatedNetworkTablesExtension.java +++ b/testing/src/main/java/com/team2813/lib2813/testing/junit/jupiter/IsolatedNetworkTablesExtension.java @@ -64,18 +64,18 @@ public void afterEach(ExtensionContext context) { Store store = context.getStore(NAMESPACE); Data data = DATA_KEY.remove(store); if (data != null) { + Preferences.setNetworkTableInstance(data.prevInstance); + // Clear out the listener queue before destroying our temporary NetworkTableInstance. // // This works around a race condition in WPILib where a listener registered by Preferences can // be called after the NetworkTableInstance was closed (see // https://github.com/wpilibsuite/allwpilib/issues/8215). - if (!data.testInstance.waitForListenerQueue(.5)) { + if (!data.testInstance.waitForListenerQueue(.2)) { System.err.println( - "Timed out waiting for the NetworkTableInstance listener queue to empty (waited 500ms);" + "Timed out waiting for the NetworkTableInstance listener queue to empty (waited 200ms);" + " JVM may crash"); } - - Preferences.setNetworkTableInstance(data.prevInstance); data.testInstance.close(); } }