[wpiutil] Add reflection based cleanup helper (#4919)

Co-authored-by: Starlight220 <53231611+Starlight220@users.noreply.github.com>
This commit is contained in:
Thad House
2023-05-12 21:35:39 -07:00
committed by GitHub
parent 15ba95df7e
commit c82fcb1975
5 changed files with 334 additions and 0 deletions

View File

@@ -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();
}
}

View File

@@ -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();
}
}
}
}
}

View File

@@ -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 {}

View File

@@ -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);
}
}

View File

@@ -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);
}
}