testing: Have GrpcCleanupRule extend ExternalResource

This allows using GrpcCleanupRule with JUnit 5 when combined with
ExternalResourceSupport. We don't really lose anything important when
running with JUnit 4 and this eases migration to JUnit 5.

ExternalResource is now responsible for combining exceptions. after()
cannot throw checked exceptions, so we must now wrap the
InterruptedException. When used with JUnit 5 we are unable to detect the
test failed; we accept that for now but it may be fair to create a new
class for JUnit 5 to be used with `@RegisterExtension` that implements
BeforeEachCallback and AfterTestExecutionCallback to restore the JUnit 4
behavior.

See #5331
This commit is contained in:
Eric Anderson 2022-06-06 07:47:52 -07:00
parent a738bc8f22
commit 97845fb72e
2 changed files with 54 additions and 50 deletions

View File

@ -26,20 +26,18 @@ import io.grpc.ExperimentalApi;
import io.grpc.ManagedChannel;
import io.grpc.Server;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.NotThreadSafe;
import org.junit.rules.TestRule;
import org.junit.rules.ExternalResource;
import org.junit.runner.Description;
import org.junit.runners.model.MultipleFailureException;
import org.junit.runners.model.Statement;
/**
* A JUnit {@link TestRule} that can register gRPC resources and manages its automatic release at
* the end of the test. If any of the resources registered to the rule can not be successfully
* released, the test will fail.
* A JUnit {@link ExternalResource} that can register gRPC resources and manages its automatic
* release at the end of the test. If any of the resources registered to the rule can not be
* successfully released, the test will fail.
*
* <p>Example usage:
* <pre>{@code @Rule public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule();
@ -73,13 +71,13 @@ import org.junit.runners.model.Statement;
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/2488")
@NotThreadSafe
public final class GrpcCleanupRule implements TestRule {
public final class GrpcCleanupRule extends ExternalResource {
private final List<Resource> resources = new ArrayList<>();
private long timeoutNanos = TimeUnit.SECONDS.toNanos(10L);
private Stopwatch stopwatch = Stopwatch.createUnstarted();
private Throwable firstException;
private boolean abruptShutdown;
/**
* Sets a positive total time limit for the automatic resource cleanup. If any of the resources
@ -144,71 +142,70 @@ public final class GrpcCleanupRule implements TestRule {
resources.add(resource);
}
// The class extends ExternalResource so it can be used in JUnit 5. But JUnit 5 will only call
// before() and after(), thus code cannot assume this method will be called.
@Override
public Statement apply(final Statement base, Description description) {
return new Statement() {
return super.apply(new Statement() {
@Override
public void evaluate() throws Throwable {
firstException = null;
abruptShutdown = false;
try {
base.evaluate();
} catch (Throwable t) {
firstException = t;
try {
teardown();
} catch (Throwable t2) {
throw new MultipleFailureException(Arrays.asList(t, t2));
}
abruptShutdown = true;
throw t;
}
teardown();
if (firstException != null) {
throw firstException;
}
}
};
}, description);
}
/**
* Releases all the registered resources.
*/
private void teardown() {
@Override
protected void after() {
stopwatch.reset();
stopwatch.start();
if (firstException == null) {
InterruptedException interrupted = null;
if (!abruptShutdown) {
for (int i = resources.size() - 1; i >= 0; i--) {
resources.get(i).cleanUp();
}
for (int i = resources.size() - 1; i >= 0; i--) {
try {
boolean released = resources.get(i).awaitReleased(
timeoutNanos - stopwatch.elapsed(TimeUnit.NANOSECONDS), TimeUnit.NANOSECONDS);
if (released) {
resources.remove(i);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
interrupted = e;
break;
}
}
}
for (int i = resources.size() - 1; i >= 0; i--) {
if (firstException != null) {
if (!resources.isEmpty()) {
for (int i = resources.size() - 1; i >= 0; i--) {
resources.get(i).forceCleanUp();
continue;
}
try {
boolean released = resources.get(i).awaitReleased(
timeoutNanos - stopwatch.elapsed(TimeUnit.NANOSECONDS), TimeUnit.NANOSECONDS);
if (!released) {
firstException = new AssertionError(
"Resource " + resources.get(i) + " can not be released in time at the end of test");
if (interrupted != null) {
throw new AssertionError(
"Thread interrupted before resources gracefully released", interrupted);
} else if (!abruptShutdown) {
throw new AssertionError(
"Resources could not be released in time at the end of test: " + resources);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
firstException = e;
}
if (firstException != null) {
resources.get(i).forceCleanUp();
} finally {
resources.clear();
}
}
resources.clear();
}
@VisibleForTesting

View File

@ -248,13 +248,13 @@ public class GrpcCleanupRuleTest {
inOrder.verify(resource3).awaitReleased(anyLong(), any(TimeUnit.class));
inOrder.verify(resource2).awaitReleased(anyLong(), any(TimeUnit.class));
inOrder.verify(resource1).awaitReleased(anyLong(), any(TimeUnit.class));
inOrder.verify(resource2).forceCleanUp();
inOrder.verify(resource1).forceCleanUp();
inOrder.verifyNoMoreInteractions();
verify(resource3, never()).forceCleanUp();
verify(resource1, never()).awaitReleased(anyLong(), any(TimeUnit.class));
verify(resource1, never()).forceCleanUp();
}
@Test
@ -280,7 +280,7 @@ public class GrpcCleanupRuleTest {
boolean cleanupFailed = false;
try {
grpcCleanup.apply(statement, null /* description*/).evaluate();
} catch (InterruptedException e) {
} catch (Throwable e) {
cleanupFailed = true;
}
@ -381,7 +381,8 @@ public class GrpcCleanupRuleTest {
@Test
public void baseTestFailsThenCleanupFails() throws Throwable {
// setup
Exception baseTestFailure = new Exception();
Exception baseTestFailure = new Exception("base test failure");
Exception cleanupFailure = new RuntimeException("force cleanup failed");
Statement statement = mock(Statement.class);
doThrow(baseTestFailure).when(statement).evaluate();
@ -389,7 +390,7 @@ public class GrpcCleanupRuleTest {
Resource resource1 = mock(Resource.class);
Resource resource2 = mock(Resource.class);
Resource resource3 = mock(Resource.class);
doThrow(new RuntimeException()).when(resource2).forceCleanUp();
doThrow(cleanupFailure).when(resource2).forceCleanUp();
InOrder inOrder = inOrder(statement, resource1, resource2, resource3);
GrpcCleanupRule grpcCleanup = new GrpcCleanupRule();
@ -407,8 +408,14 @@ public class GrpcCleanupRuleTest {
}
// verify
assertThat(failure).isInstanceOf(MultipleFailureException.class);
assertSame(baseTestFailure, ((MultipleFailureException) failure).getFailures().get(0));
if (failure instanceof MultipleFailureException) {
// JUnit 4.13+
assertThat(((MultipleFailureException) failure).getFailures())
.containsExactly(baseTestFailure, cleanupFailure);
} else {
// JUnit 4.12. Suffers from https://github.com/junit-team/junit4/issues/1334
assertThat(failure).isSameInstanceAs(cleanupFailure);
}
inOrder.verify(statement).evaluate();
inOrder.verify(resource3).forceCleanUp();