Skip to content

Commit aaf91ce

Browse files
committed
Add AutoCleanupExtension deferAfterAll
1 parent 1f42f07 commit aaf91ce

7 files changed

Lines changed: 80 additions & 64 deletions

File tree

.github/agents/knowledge/testing-general-patterns.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,19 @@
1717
- In JUnit tests, when an `AutoCloseable` is intended to remain live for most or all of the test
1818
and only needs cleanup at test end, prefer `AutoCleanupExtension` with `deferCleanup(...)`
1919
over wrapping most of the test body in try-with-resources.
20+
- For resources created in `@BeforeAll` or other class-scoped setup, prefer
21+
`AutoCleanupExtension` with `deferAfterAll(...)` over nested `@AfterAll` cleanup
22+
chains.
2023
- Reuse an existing `cleanup` extension when one is already in scope.
2124
Otherwise, add a `@RegisterExtension` field when the deferred-cleanup pattern improves
2225
clarity or avoids wrapping most of the test body.
2326
- Keep try-with-resources for semantically scoped resources whose lifetime must end before the
2427
rest of the test continues, such as `Scope` / `Context.makeCurrent()`, Mockito
2528
`MockedStatic`, and short-lived readers, writers, streams, response bodies, or parsers.
26-
- Do not use `AutoCleanupExtension` in non-JUnit helper methods, `@BeforeAll`, or other shared
27-
setup code where cleanup is not tied to a single test method.
29+
- Keep `AutoCleanupExtension` usage scoped to JUnit-managed test classes; do not introduce it in
30+
generic helper utilities or other non-JUnit code.
31+
- In `@BeforeAll` or other shared setup code where cleanup is not tied to a single test method,
32+
use `deferAfterAll(...)` instead of `deferCleanup(...)`.
2833
- If the test intentionally closes the resource mid-test or asserts behavior around explicit
2934
close, keep the direct close or try-with-resources in the test body.
3035

instrumentation/kafka/kafka-clients/kafka-clients-0.11/testing/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaClientBaseTest.java

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import static org.assertj.core.api.Assertions.assertThat;
2929

3030
import io.opentelemetry.api.common.AttributeKey;
31+
import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension;
3132
import io.opentelemetry.sdk.testing.assertj.AttributeAssertion;
3233
import java.time.Duration;
3334
import java.util.ArrayList;
@@ -53,9 +54,9 @@
5354
import org.apache.kafka.common.serialization.StringSerializer;
5455
import org.assertj.core.api.AbstractLongAssert;
5556
import org.assertj.core.api.AbstractStringAssert;
56-
import org.junit.jupiter.api.AfterAll;
5757
import org.junit.jupiter.api.BeforeAll;
5858
import org.junit.jupiter.api.TestInstance;
59+
import org.junit.jupiter.api.extension.RegisterExtension;
5960
import org.slf4j.Logger;
6061
import org.slf4j.LoggerFactory;
6162
import org.testcontainers.containers.output.Slf4jLogConsumer;
@@ -67,6 +68,8 @@
6768
public abstract class KafkaClientBaseTest {
6869
private static final Logger logger = LoggerFactory.getLogger(KafkaClientBaseTest.class);
6970

71+
@RegisterExtension final AutoCleanupExtension cleanup = AutoCleanupExtension.create();
72+
7073
protected static final String SHARED_TOPIC = "shared.topic";
7174
protected static final AttributeKey<String> MESSAGING_CLIENT_ID =
7275
AttributeKey.stringKey("messaging.client_id");
@@ -84,13 +87,15 @@ public abstract class KafkaClientBaseTest {
8487

8588
@BeforeAll
8689
void setupClass() throws ExecutionException, InterruptedException, TimeoutException {
87-
kafka =
90+
KafkaContainer kafkaContainer =
8891
new KafkaContainer(DockerImageName.parse("apache/kafka:3.8.0"))
8992
.withEnv("KAFKA_HEAP_OPTS", "-Xmx256m")
9093
.withLogConsumer(new Slf4jLogConsumer(logger))
9194
.waitingFor(Wait.forLogMessage(".*started \\(kafka.server.Kafka.*Server\\).*", 1))
9295
.withStartupTimeout(Duration.ofMinutes(1));
93-
kafka.start();
96+
kafka = kafkaContainer;
97+
cleanup.deferAfterAll(kafkaContainer::stop);
98+
kafkaContainer.start();
9499

95100
// create test topic
96101
HashMap<String, Object> adminProps = new HashMap<>();
@@ -104,8 +109,10 @@ void setupClass() throws ExecutionException, InterruptedException, TimeoutExcept
104109
}
105110

106111
producer = new KafkaProducer<>(producerProps());
112+
cleanup.deferAfterAll(producer);
107113

108114
consumer = new KafkaConsumer<>(consumerProps());
115+
cleanup.deferAfterAll(consumer);
109116

110117
consumer.subscribe(
111118
singletonList(SHARED_TOPIC),
@@ -144,17 +151,6 @@ public Map<String, Object> producerProps() {
144151
return props;
145152
}
146153

147-
@AfterAll
148-
void cleanupClass() {
149-
if (producer != null) {
150-
producer.close();
151-
}
152-
if (consumer != null) {
153-
consumer.close();
154-
}
155-
kafka.stop();
156-
}
157-
158154
public void awaitUntilConsumerIsReady() throws InterruptedException {
159155
if (consumerReady.await(0, SECONDS)) {
160156
return;

instrumentation/opensearch/opensearch-rest-common/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/rest/AbstractOpenSearchRestTest.java

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,16 @@
2424
import static org.assertj.core.api.Assertions.assertThat;
2525

2626
import io.opentelemetry.api.trace.SpanKind;
27+
import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension;
2728
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
2829
import java.io.IOException;
2930
import java.net.URI;
3031
import java.util.concurrent.CountDownLatch;
3132
import java.util.concurrent.atomic.AtomicReference;
32-
import org.junit.jupiter.api.AfterAll;
3333
import org.junit.jupiter.api.BeforeAll;
3434
import org.junit.jupiter.api.Test;
3535
import org.junit.jupiter.api.TestInstance;
36+
import org.junit.jupiter.api.extension.RegisterExtension;
3637
import org.opensearch.client.Request;
3738
import org.opensearch.client.Response;
3839
import org.opensearch.client.ResponseListener;
@@ -44,6 +45,8 @@
4445
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
4546
public abstract class AbstractOpenSearchRestTest {
4647

48+
@RegisterExtension final AutoCleanupExtension cleanup = AutoCleanupExtension.create();
49+
4750
protected OpensearchContainer opensearch;
4851
protected RestClient client;
4952
protected URI httpHost;
@@ -58,26 +61,20 @@ public abstract class AbstractOpenSearchRestTest {
5861

5962
@BeforeAll
6063
void setUp() throws Exception {
61-
opensearch =
64+
OpensearchContainer opensearchContainer =
6265
new OpensearchContainer(DockerImageName.parse("opensearchproject/opensearch:1.3.6"))
6366
.withSecurityEnabled();
67+
opensearch = opensearchContainer;
68+
cleanup.deferAfterAll(opensearchContainer::stop);
6469
// limit memory usage and disable Log4j JMX to avoid cgroup detection issues in containers
65-
opensearch.withEnv(
70+
opensearchContainer.withEnv(
6671
"OPENSEARCH_JAVA_OPTS",
6772
"-Xmx256m -Xms256m -Dlog4j2.disableJmx=true -Dlog4j2.disable.jmx=true -XX:-UseContainerSupport");
68-
opensearch.start();
69-
httpHost = URI.create(opensearch.getHttpHostAddress());
73+
opensearchContainer.start();
74+
httpHost = URI.create(opensearchContainer.getHttpHostAddress());
7075

7176
client = buildRestClient();
72-
}
73-
74-
@AfterAll
75-
void tearDown() throws IOException {
76-
try {
77-
client.close();
78-
} finally {
79-
opensearch.stop();
80-
}
77+
cleanup.deferAfterAll(client);
8178
}
8279

8380
@Test

instrumentation/reactor/reactor-kafka-1.0/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/reactor/kafka/v1_0/AbstractReactorKafkaTest.java

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
import org.apache.kafka.common.serialization.StringSerializer;
4646
import org.assertj.core.api.AbstractLongAssert;
4747
import org.assertj.core.api.AbstractStringAssert;
48-
import org.junit.jupiter.api.AfterAll;
4948
import org.junit.jupiter.api.BeforeAll;
5049
import org.junit.jupiter.api.extension.RegisterExtension;
5150
import org.slf4j.Logger;
@@ -80,31 +79,21 @@ public abstract class AbstractReactorKafkaTest {
8079

8180
@BeforeAll
8281
static void setUpAll() {
83-
kafka =
82+
KafkaContainer kafkaContainer =
8483
new KafkaContainer(DockerImageName.parse("apache/kafka:3.8.0"))
8584
.withEnv("KAFKA_HEAP_OPTS", "-Xmx256m")
8685
.withLogConsumer(new Slf4jLogConsumer(logger))
8786
.waitingFor(Wait.forLogMessage(".*started \\(kafka.server.Kafka.*Server\\).*", 1))
8887
.withStartupTimeout(Duration.ofMinutes(1));
89-
kafka.start();
88+
kafka = kafkaContainer;
89+
cleanup.deferAfterAll(kafkaContainer::stop);
90+
kafkaContainer.start();
9091

9192
sender = KafkaSender.create(senderOptions());
93+
cleanup.deferAfterAll(sender::close);
9294
receiver = KafkaReceiver.create(receiverOptions());
9395
}
9496

95-
@AfterAll
96-
static void tearDownAll() {
97-
try {
98-
if (sender != null) {
99-
sender.close();
100-
}
101-
} finally {
102-
if (kafka != null) {
103-
kafka.stop();
104-
}
105-
}
106-
}
107-
10897
@SuppressWarnings("unchecked")
10998
private static SenderOptions<String, String> senderOptions() {
11099
Properties props = new Properties();

instrumentation/spring/spring-pulsar-1.0/testing/src/main/java/io/opentelemetry/instrumentation/spring/pulsar/v1_0/AbstractSpringPulsarTest.java

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import static org.assertj.core.api.Assertions.assertThat;
2222

2323
import io.opentelemetry.instrumentation.testing.GlobalTraceUtil;
24+
import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension;
2425
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
2526
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
2627
import io.opentelemetry.sdk.testing.assertj.AttributeAssertion;
@@ -33,7 +34,6 @@
3334
import org.apache.pulsar.client.api.PulsarClientException;
3435
import org.assertj.core.api.AbstractLongAssert;
3536
import org.assertj.core.api.AbstractStringAssert;
36-
import org.junit.jupiter.api.AfterAll;
3737
import org.junit.jupiter.api.BeforeAll;
3838
import org.junit.jupiter.api.Test;
3939
import org.junit.jupiter.api.extension.RegisterExtension;
@@ -52,6 +52,9 @@ public abstract class AbstractSpringPulsarTest {
5252
@RegisterExtension
5353
protected static final InstrumentationExtension testing = AgentInstrumentationExtension.create();
5454

55+
@RegisterExtension
56+
static final AutoCleanupExtension cleanup = AutoCleanupExtension.create();
57+
5558
private static final boolean EXPERIMENTAL_ATTRIBUTES =
5659
Boolean.getBoolean("otel.instrumentation.pulsar.experimental-span-attributes");
5760
private static final DockerImageName DEFAULT_IMAGE_NAME =
@@ -74,6 +77,7 @@ static void setUp() throws PulsarClientException {
7477
new PulsarContainer(DEFAULT_IMAGE_NAME)
7578
.withEnv("PULSAR_MEM", "-Xmx128m")
7679
.withStartupTimeout(Duration.ofMinutes(2));
80+
cleanup.deferAfterAll(pulsarContainer::stop);
7781
pulsarContainer.start();
7882
brokerHost = pulsarContainer.getHost();
7983
brokerPort = pulsarContainer.getMappedPort(6650);
@@ -85,9 +89,11 @@ static void setUp() throws PulsarClientException {
8589
props.put("spring.pulsar.consumer.subscription.initial-position", "earliest");
8690
app.setDefaultProperties(props);
8791
applicationContext = app.run();
92+
cleanup.deferAfterAll(applicationContext);
8893
pulsarTemplate = applicationContext.getBean(PulsarTemplate.class);
8994

9095
client = PulsarClient.builder().serviceUrl(pulsarContainer.getPulsarBrokerUrl()).build();
96+
cleanup.deferAfterAll(client);
9197
}
9298

9399
@Test
@@ -101,22 +107,6 @@ void testSpringPulsar() throws PulsarClientException, InterruptedException {
101107
assertSpringPulsar();
102108
}
103109

104-
@AfterAll
105-
static void teardown() throws PulsarClientException {
106-
if (applicationContext != null) {
107-
applicationContext.close();
108-
}
109-
try {
110-
if (client != null) {
111-
client.close();
112-
}
113-
} finally {
114-
if (pulsarContainer != null) {
115-
pulsarContainer.stop();
116-
}
117-
}
118-
}
119-
120110
protected abstract void assertSpringPulsar();
121111

122112
protected List<AttributeAssertion> publishAttributes() {

testing-common/src/main/java/io/opentelemetry/instrumentation/testing/internal/AutoCleanupExtension.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66
package io.opentelemetry.instrumentation.testing.internal;
77

88
import java.util.ArrayList;
9+
import java.util.Deque;
910
import java.util.List;
1011
import java.util.Queue;
12+
import java.util.concurrent.ConcurrentLinkedDeque;
1113
import java.util.concurrent.ConcurrentLinkedQueue;
14+
import org.junit.jupiter.api.extension.AfterAllCallback;
1215
import org.junit.jupiter.api.extension.AfterEachCallback;
1316
import org.junit.jupiter.api.extension.ExtensionContext;
1417

@@ -20,8 +23,9 @@
2023
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
2124
* at any time.
2225
*/
23-
public class AutoCleanupExtension implements AfterEachCallback {
26+
public class AutoCleanupExtension implements AfterEachCallback, AfterAllCallback {
2427
private final Queue<AutoCloseable> thingsToCleanUp = new ConcurrentLinkedQueue<>();
28+
private final Deque<AutoCloseable> thingsToCleanUpAfterAll = new ConcurrentLinkedDeque<>();
2529

2630
private AutoCleanupExtension() {}
2731

@@ -34,6 +38,11 @@ public void deferCleanup(AutoCloseable cleanupAction) {
3438
thingsToCleanUp.add(cleanupAction);
3539
}
3640

41+
/** Add a {@code cleanupAction} to execute after the test class finishes. */
42+
public void deferAfterAll(AutoCloseable cleanupAction) {
43+
thingsToCleanUpAfterAll.push(cleanupAction);
44+
}
45+
3746
@Override
3847
public void afterEach(ExtensionContext extensionContext) throws Exception {
3948
List<Exception> exceptions = new ArrayList<>();
@@ -44,7 +53,23 @@ public void afterEach(ExtensionContext extensionContext) throws Exception {
4453
exceptions.add(e);
4554
}
4655
}
56+
throwIfAny(exceptions);
57+
}
58+
59+
@Override
60+
public void afterAll(ExtensionContext extensionContext) throws Exception {
61+
List<Exception> exceptions = new ArrayList<>();
62+
while (!thingsToCleanUpAfterAll.isEmpty()) {
63+
try {
64+
thingsToCleanUpAfterAll.pop().close();
65+
} catch (Exception e) {
66+
exceptions.add(e);
67+
}
68+
}
69+
throwIfAny(exceptions);
70+
}
4771

72+
private static void throwIfAny(List<Exception> exceptions) throws Exception {
4873
switch (exceptions.size()) {
4974
case 0:
5075
return;

testing-common/src/test/java/io/opentelemetry/instrumentation/testing/junit/AutoCleanupExtensionTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,18 @@ void shouldRunCleanupAfterTest() {
3030
static void verifyCount() {
3131
assertThat(count.get()).isEqualTo(1);
3232
}
33+
34+
@Test
35+
void shouldRunCleanupAfterAll() throws Exception {
36+
AutoCleanupExtension cleanup = AutoCleanupExtension.create();
37+
AtomicInteger countAfterAll = new AtomicInteger(0);
38+
39+
cleanup.deferAfterAll(countAfterAll::incrementAndGet);
40+
41+
assertThat(countAfterAll.get()).isEqualTo(0);
42+
43+
cleanup.afterAll(null);
44+
45+
assertThat(countAfterAll.get()).isEqualTo(1);
46+
}
3347
}

0 commit comments

Comments
 (0)