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,80 @@
package com.team2813.lib2813.testing.junit.jupiter;

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.Extension;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.ExtensionContext.Store;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;

/**
* JUnit Jupiter extension for providing an isolated NetworkTableInstance to tests.
*
* <p>Example use:
*
* <pre>{@code
* @ExtendWith(IsolatedNetworkTablesExtension.class)
* public final class IntakeTest {
*
* @Test
* public void intakeCoral(NetworkTableInstance ntInstance) {
* // Do something with ntInstance
* }
* }
* }</pre>
*/
public final class IsolatedNetworkTablesExtension
implements Extension, AfterEachCallback, ParameterResolver {
private static final StoreKey<NetworkTableInstance> 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.get(getStore(context));
if (ntInstance != 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(.1)) {
System.err.println(
"Timed out waiting for the NetworkTableInstance listener queue to empty (waited 100ms);"
+ " JVM may crash");
}

Preferences.setNetworkTableInstance(NetworkTableInstance.getDefault());
ntInstance.close();
}
}

@Override
public boolean supportsParameter(
ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
return NetworkTableInstance.class.equals(parameterContext.getParameter().getType());
}

@Override
public NetworkTableInstance resolveParameter(
ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
Store store = getStore(extensionContext);
NetworkTableInstance ntInstance =
NETWORK_TABLE_INSTANCE_KEY.getOrComputeIfAbsent(store, NetworkTableInstance::create);

ntInstance.startLocal();
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()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.team2813.lib2813.testing.junit.jupiter;

import java.util.function.Supplier;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Store;

/**
* A type-safe key for use in a {@link Store}.
*
* @param <V> the type for values stored in this store.
*/
final class StoreKey<V> {
private final Class<V> requiredType;

/**
* Creates a key for storing data of the provided type.
*
* <p>Values returned from this method should almost always be stored in static final fields.
*/
public static <V> StoreKey<V> of(Class<V> requiredType) {
return new StoreKey<>(requiredType);
}

private StoreKey(Class<V> requiredType) {
this.requiredType = requiredType;
}

/**
* Gets the value that is stored under this key.
*
* <p>If no value is stored in the current {@link ExtensionContext} for this key, ancestors of the
* context will be queried for a value with this key in the {@code Namespace} used to create this
* store.
*
* @param store the store to get data from.
* @see #getOrDefault(Store, V)
*/
public V get(Store store) {
return store.get(this, requiredType);
}

/**
* Gets the value of the specified required type that is stored under this key, or the supplied
* {@code defaultValue} if no value is found for this key in this store or in an ancestor.
*
* <p>If no value is stored in the current {@link ExtensionContext} for this, ancestors of the
* context will be queried for a value with this key in the {@code Namespace} used to create this
* store.
*
* @param store the store to get data from.
* @param defaultValue the default value.
* @return the value; potentially {@code null}.
* @see #get(Store)
*/
public V getOrDefault(Store store, V defaultValue) {
return store.getOrDefault(this, requiredType, defaultValue);
}

/**
* Gets the value of the specified required type that is stored under this key.
*
* <p>If no value is stored in the current {@link ExtensionContext} for this key, ancestors of the
* context will be queried for a value with this key in the {@code Namespace} used to create this
* store. If no value is found for this key a new value will be computed by the {@code
* valueSupplier}, stored, and returned.
*
* <p>If {@code requiredType} implements {@link Store.CloseableResource} or {@link AutoCloseable}
* (unless the {@code junit.jupiter.extensions.store.close.autocloseable.enabled} configuration
* parameter is set to {@code false}), then the {@code close()} method will be invoked on the
* stored object when the store is closed.
*
* @param store the store to use to get and store the data.
* @param valueSupplier the function called to create a new value; never {@code null} but may
* return {@code null}.
* @return the value; potentially {@code null}.
* @see Store.CloseableResource
* @see AutoCloseable
*/
public V getOrComputeIfAbsent(Store store, Supplier<V> valueSupplier) {
return store.getOrComputeIfAbsent(this, key -> valueSupplier.get(), requiredType);
}

/**
* Stores a {@code value} for later retrieval under this key.
*
* <p>A stored {@code value} is visible in child {@link ExtensionContext ExtensionContexts} for
* the store's {@code Namespace} unless they overwrite it.
*
* <p>If the {@code value} is an instance of {@link Store.CloseableResource} or {@link
* AutoCloseable} (unless the {@code junit.jupiter.extensions.store.close.autocloseable.enabled}
* configuration parameter is set to {@code false}), then the {@code close()} method will be
* invoked on the stored object when the store is closed.
*
* @param store the store to put data into.
* @param value the value to store; may be {@code null}.
* @see Store.CloseableResource
* @see AutoCloseable
*/
public void put(Store store, V value) {
store.put(this, value);
}

/**
* Removes the value of the specified required type that was previously stored under this key.
*
* <p>The value will only be removed in the current {@link ExtensionContext}, not in ancestors. In
* addition, the {@link Store.CloseableResource} and {@link AutoCloseable} API will not be honored
* for values that are manually removed via this method.
*
* @param store the store to remove data from.
* @return the previous value or {@code null} if no value was present for the specified key.
*/
public V remove(Store store) {
return store.remove(this, requiredType);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.team2813.lib2813.testing.junit.jupiter;

import org.junit.platform.testkit.engine.EngineExecutionResults;
import org.junit.platform.testkit.engine.Events;

/**
* A collection of utility methods that support asserting conditions in tests of JUnit Extensions.
*/
final class ExtensionAssertions {

/**
* Asserts that the supplied {@code events} do not contain any failures.
*
* @param events Events fired during execution of a test plan on the JUnit Platform.
*/
public static void assertHasNoFailures(Events events) {
events.assertStatistics(
stats -> {
stats.skipped(0);
stats.failed(0);
});
}

/**
* Asserts that the supplied {@code results} do not contain any failures.
*
* @param results Results of executing a test plan on the JUnit Platform.
*/
public static void assertHasNoFailures(EngineExecutionResults results) {
assertHasNoFailures(results.containerEvents());
assertHasNoFailures(results.testEvents());
}

private ExtensionAssertions() {
throw new AssertionError("Not instantiable");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.team2813.lib2813.testing.junit.jupiter;

import static com.google.common.truth.Truth.assertThat;
import static com.team2813.lib2813.testing.junit.jupiter.ExtensionAssertions.assertHasNoFailures;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;

import edu.wpi.first.networktables.NetworkTableInstance;
import org.junit.jupiter.api.MethodOrderer;
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;

/** Tests for {@link IsolatedNetworkTablesExtension}. */
class IsolatedNetworkTablesExtensionTest {

@ExtendWith(IsolatedNetworkTablesExtension.class)
@Tag("ignore-outside-testkit")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public static class SampleTest {

@Test
public void verifyProvidesNetworkTableParameter(NetworkTableInstance ntInstance) {
assertThat(ntInstance).isNotNull();
assertThat(ntInstance.getHandle())
.isNotEqualTo(NetworkTableInstance.getDefault().getHandle());
}
} // end SampleTest

@Test
void verifyExtension() {
// Act - Run tests in SampleTest
EngineExecutionResults results =
EngineTestKit.engine("junit-jupiter").selectors(selectClass(SampleTest.class)).execute();

// Assert - All tests in SampleTest pass
assertHasNoFailures(results);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.team2813.lib2813.testing.junit.jupiter.ExtensionAssertions.assertHasNoFailures;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;

import edu.wpi.first.hal.HAL;
Expand All @@ -20,7 +21,6 @@
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 {
Expand Down Expand Up @@ -129,19 +129,6 @@ private void withDriverStationTemporarilyEnabled(Runnable runnable) {
}
}

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;
Expand Down
Loading