mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-06-27 02:01:42 +00:00
[wpiutil] Add reflection based cleanup helper (#4919)
Co-authored-by: Starlight220 <53231611+Starlight220@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.util.cleanup;
|
||||
|
||||
import edu.wpi.first.util.ErrorMessages;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
|
||||
/**
|
||||
* An object containing a Stack of AutoCloseable objects that are closed when this object is closed.
|
||||
*/
|
||||
public class CleanupPool implements AutoCloseable {
|
||||
// Use a Deque instead of a Stack, as Stack's iterators go the wrong way, and docs
|
||||
// state ArrayDeque is faster anyway.
|
||||
private final Deque<AutoCloseable> m_closers = new ArrayDeque<AutoCloseable>();
|
||||
|
||||
/**
|
||||
* Registers an object in the object stack for cleanup.
|
||||
*
|
||||
* @param <T> The object type
|
||||
* @param object The object to register
|
||||
* @return The registered object
|
||||
*/
|
||||
public <T extends AutoCloseable> T register(T object) {
|
||||
ErrorMessages.requireNonNullParam(object, "object", "register");
|
||||
m_closers.addFirst(object);
|
||||
return object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an object from the cleanup stack.
|
||||
*
|
||||
* @param object the object to remove
|
||||
*/
|
||||
public void remove(AutoCloseable object) {
|
||||
m_closers.remove(object);
|
||||
}
|
||||
|
||||
/** Closes all objects in the stack. */
|
||||
@Override
|
||||
@SuppressWarnings("PMD.AvoidCatchingGenericException")
|
||||
public void close() {
|
||||
for (AutoCloseable autoCloseable : m_closers) {
|
||||
try {
|
||||
autoCloseable.close();
|
||||
} catch (Exception e) {
|
||||
// Swallow any exceptions on close
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
m_closers.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.util.cleanup;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
/**
|
||||
* Implement this interface to have access to a `reflectionCleanup` method that can be called from
|
||||
* your `close` method, that will use reflection to find all `AutoCloseable` instance members and
|
||||
* close them.
|
||||
*/
|
||||
public interface ReflectionCleanup extends AutoCloseable {
|
||||
/**
|
||||
* Default implementation that uses reflection to find all AutoCloseable fields not marked
|
||||
* SkipCleanup and call close() on them. Call this from your `close()` method with the class level
|
||||
* you want to close.
|
||||
*
|
||||
* @param cls the class level to clean up
|
||||
*/
|
||||
@SuppressWarnings("PMD.AvoidCatchingGenericException")
|
||||
default void reflectionCleanup(Class<? extends ReflectionCleanup> cls) {
|
||||
if (!cls.isAssignableFrom(getClass())) {
|
||||
System.out.println("Passed in class is not assignable from \"this\"");
|
||||
System.out.println("Expected something in the hierarchy of" + cls.getName());
|
||||
System.out.println("This is " + getClass().getName());
|
||||
return;
|
||||
}
|
||||
for (Field field : cls.getDeclaredFields()) {
|
||||
if (field.isAnnotationPresent(SkipCleanup.class)) {
|
||||
continue;
|
||||
}
|
||||
if (!AutoCloseable.class.isAssignableFrom(field.getType())) {
|
||||
continue;
|
||||
}
|
||||
if (field.trySetAccessible()) {
|
||||
try {
|
||||
AutoCloseable c = (AutoCloseable) field.get(this);
|
||||
if (c != null) {
|
||||
c.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Ignore any exceptions
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.util.cleanup;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target(ElementType.FIELD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface SkipCleanup {}
|
||||
@@ -0,0 +1,154 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.util.cleanup;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class CleanupPoolTest {
|
||||
static class AutoCloseableObject implements AutoCloseable {
|
||||
public boolean m_closed;
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
m_closed = true;
|
||||
}
|
||||
}
|
||||
|
||||
static class AutoCloseableObjectWithCallback implements AutoCloseable {
|
||||
private final Runnable m_cb;
|
||||
|
||||
AutoCloseableObjectWithCallback(Runnable cb) {
|
||||
m_cb = cb;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
m_cb.run();
|
||||
}
|
||||
}
|
||||
|
||||
static class FailingAutoCloseableObject implements AutoCloseable {
|
||||
public static final String message = "This is an expected failure";
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
throw new RuntimeException(message);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void cleanupStackWorks() {
|
||||
List<AutoCloseableObject> objects = new ArrayList<>();
|
||||
objects.add(new AutoCloseableObject());
|
||||
objects.add(new AutoCloseableObject());
|
||||
objects.add(new AutoCloseableObject());
|
||||
|
||||
try (CleanupPool pool = new CleanupPool()) {
|
||||
for (AutoCloseableObject autoCloseableObject : objects) {
|
||||
pool.register(autoCloseableObject);
|
||||
}
|
||||
}
|
||||
|
||||
for (AutoCloseableObject autoCloseableObject : objects) {
|
||||
assertTrue(autoCloseableObject.m_closed);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("PMD.AvoidCatchingGenericException")
|
||||
void cleanupStackWithExceptionNotInCloseWorks() {
|
||||
List<AutoCloseableObject> objects = new ArrayList<>();
|
||||
objects.add(new AutoCloseableObject());
|
||||
objects.add(new AutoCloseableObject());
|
||||
objects.add(new AutoCloseableObject());
|
||||
|
||||
String message = "This is a known failure";
|
||||
|
||||
try (CleanupPool pool = new CleanupPool()) {
|
||||
for (AutoCloseableObject autoCloseableObject : objects) {
|
||||
pool.register(autoCloseableObject);
|
||||
}
|
||||
throw new Exception(message);
|
||||
} catch (Exception e) {
|
||||
assertEquals(message, e.getMessage());
|
||||
}
|
||||
|
||||
for (AutoCloseableObject autoCloseableObject : objects) {
|
||||
assertTrue(autoCloseableObject.m_closed);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("PMD.AvoidCatchingGenericException")
|
||||
void cleanupStackWithExceptionInCloseWorks() {
|
||||
List<AutoCloseableObject> objects = new ArrayList<>();
|
||||
objects.add(new AutoCloseableObject());
|
||||
objects.add(new AutoCloseableObject());
|
||||
objects.add(new AutoCloseableObject());
|
||||
|
||||
try (CleanupPool pool = new CleanupPool()) {
|
||||
for (AutoCloseableObject autoCloseableObject : objects) {
|
||||
pool.register(new FailingAutoCloseableObject());
|
||||
pool.register(autoCloseableObject);
|
||||
}
|
||||
}
|
||||
|
||||
for (AutoCloseableObject autoCloseableObject : objects) {
|
||||
assertTrue(autoCloseableObject.m_closed);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void cleanupStackRemovalWorks() {
|
||||
List<AutoCloseableObject> objects = new ArrayList<>();
|
||||
objects.add(new AutoCloseableObject());
|
||||
objects.add(new AutoCloseableObject());
|
||||
objects.add(new AutoCloseableObject());
|
||||
|
||||
try (CleanupPool pool = new CleanupPool()) {
|
||||
for (AutoCloseableObject autoCloseableObject : objects) {
|
||||
pool.register(autoCloseableObject);
|
||||
}
|
||||
|
||||
pool.remove(objects.get(0));
|
||||
}
|
||||
|
||||
int idx = 0;
|
||||
for (AutoCloseableObject autoCloseableObject : objects) {
|
||||
if (idx == 0) {
|
||||
assertFalse(autoCloseableObject.m_closed);
|
||||
} else {
|
||||
assertTrue(autoCloseableObject.m_closed);
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void cleanupStackIsLifo() {
|
||||
List<AutoCloseableObjectWithCallback> objects = new ArrayList<>();
|
||||
List<Integer> order = new ArrayList<>();
|
||||
objects.add(new AutoCloseableObjectWithCallback(() -> order.add(0)));
|
||||
objects.add(new AutoCloseableObjectWithCallback(() -> order.add(1)));
|
||||
objects.add(new AutoCloseableObjectWithCallback(() -> order.add(2)));
|
||||
|
||||
try (CleanupPool pool = new CleanupPool()) {
|
||||
for (AutoCloseable autoCloseableObject : objects) {
|
||||
pool.register(autoCloseableObject);
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(order.size(), 3);
|
||||
assertEquals(order.get(0), 2);
|
||||
assertEquals(order.get(1), 1);
|
||||
assertEquals(order.get(2), 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.util.cleanup;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ReflectionCleanupTest {
|
||||
static class CleanupClass implements AutoCloseable {
|
||||
public boolean m_closed;
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
m_closed = true;
|
||||
}
|
||||
}
|
||||
|
||||
static class CleanupTest implements ReflectionCleanup {
|
||||
public CleanupClass m_class1 = new CleanupClass();
|
||||
public CleanupClass m_class2 = new CleanupClass();
|
||||
public Object m_nonCleanupObject = new Object();
|
||||
public Object m_nullCleanupObject;
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
reflectionCleanup(CleanupTest.class);
|
||||
}
|
||||
}
|
||||
|
||||
static class CleanupTest2 extends CleanupTest {
|
||||
@SkipCleanup public CleanupClass m_class3 = new CleanupClass();
|
||||
public CleanupClass m_class4 = new CleanupClass();
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
reflectionCleanup(CleanupTest2.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void cleanupClosesAllFields() {
|
||||
CleanupTest test = new CleanupTest();
|
||||
test.close();
|
||||
assertTrue(test.m_class1.m_closed);
|
||||
assertTrue(test.m_class2.m_closed);
|
||||
}
|
||||
|
||||
@Test
|
||||
void cleanupOnlyClosesExplicitClassAndSkipWorks() {
|
||||
CleanupTest2 test = new CleanupTest2();
|
||||
test.close();
|
||||
assertFalse(test.m_class1.m_closed);
|
||||
assertFalse(test.m_class2.m_closed);
|
||||
assertFalse(test.m_class3.m_closed);
|
||||
assertTrue(test.m_class4.m_closed);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user