Repository: junit-team/junit-framework (2025-07-19T13:39:49Z)

Issue:
Reduce number of logged warnings for type implementing `CloseableResource` but not `AutoCloseable`
Starting with the 5.13 release, JUnit now spams the following warning hundreds of times per build:
```
WARNING: Type implements CloseableResource but not AutoCloseable
```

We use the [Testcontainers](https://github.com/testcontainers/testcontainers-java) framework in [JSch](https://github.com/mwiede/jsch).

This can be seen in our Github Actions build logs, as seen [here](https://github.com/mwiede/jsch/actions/runs/16289594620/job/45996256405#step:7:1509) for example.

## Steps to reproduce

Use [Testcontainers](https://github.com/testcontainers/testcontainers-java) extension and note how it spams the warning hundreds of times per build.

## Context

 - Used versions (Jupiter/Vintage/Platform): 5.13.3
 - Build Tool/IDE: Maven

## Deliverables

- [x] Reduce logging to once per `CloseableResource` implementation class



Discussion:
I've also opened testcontainers/testcontainers-java#10525 as an enhancement request to update their JUnit extension to implement AutoCloseable as now recommended.



Patch:
diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.4.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.4.adoc
index 54b791e7d66f..d5d027c47759 100644
--- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.4.adoc
+++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.4.adoc
@@ -58,7 +58,8 @@ repository on GitHub.
 [[release-notes-5.13.4-junit-jupiter-new-features-and-improvements]]
 ==== New Features and Improvements
 
-* ❓
+* Log only once per implementation type for `CloseableResource` implementations that do
+  not implement `AutoCloseable` to avoid flooding console output with this warning.
 
 
 [[release-notes-5.13.4-junit-vintage]]
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java
index d19c71845461..c028255b36f9 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java
@@ -54,6 +54,8 @@
 abstract class AbstractExtensionContext<T extends TestDescriptor> implements ExtensionContextInternal, AutoCloseable {
 
 	private static final Logger LOGGER = LoggerFactory.getLogger(AbstractExtensionContext.class);
+	private static final Namespace CLOSEABLE_RESOURCE_LOGGING_NAMESPACE = Namespace.create(
+		AbstractExtensionContext.class, "CloseableResourceLogging");
 
 	private final @Nullable ExtensionContext parent;
 	private final EngineExecutionListener engineExecutionListener;
@@ -86,42 +88,39 @@ abstract class AbstractExtensionContext<T extends TestDescriptor> implements Ext
 				.collect(collectingAndThen(toCollection(LinkedHashSet::new), Collections::unmodifiableSet));
 		// @formatter:on
 
-		this.valuesStore = createStore(parent, launcherStoreFacade, createCloseAction());
+		this.valuesStore = new NamespacedHierarchicalStore<>(getParentStore(parent), createCloseAction());
+	}
+
+	private NamespacedHierarchicalStore<org.junit.platform.engine.support.store.Namespace> getParentStore(
+			@Nullable ExtensionContext parent) {
+		return parent == null //
+				? this.launcherStoreFacade.getRequestLevelStore() //
+				: ((AbstractExtensionContext<?>) parent).valuesStore;
 	}
 
 	@SuppressWarnings("deprecation")
-	private NamespacedHierarchicalStore.CloseAction<org.junit.platform.engine.support.store.Namespace> createCloseAction() {
+	private <N> NamespacedHierarchicalStore.CloseAction<N> createCloseAction() {
+		var store = this.launcherStoreFacade.getSessionLevelStore(CLOSEABLE_RESOURCE_LOGGING_NAMESPACE);
 		return (__, ___, value) -> {
 			boolean isAutoCloseEnabled = this.configuration.isClosingStoredAutoCloseablesEnabled();
 
-			if (value instanceof @SuppressWarnings("resource") AutoCloseable closeable && isAutoCloseEnabled) {
+			if (isAutoCloseEnabled && value instanceof @SuppressWarnings("resource") AutoCloseable closeable) {
 				closeable.close();
 				return;
 			}
 
 			if (value instanceof Store.CloseableResource resource) {
 				if (isAutoCloseEnabled) {
-					LOGGER.warn(
-						() -> "Type implements CloseableResource but not AutoCloseable: " + value.getClass().getName());
+					store.computeIfAbsent(value.getClass(), type -> {
+						LOGGER.warn(() -> "Type implements CloseableResource but not AutoCloseable: " + type.getName());
+						return true;
+					});
 				}
 				resource.close();
 			}
 		};
 	}
 
-	private static NamespacedHierarchicalStore<org.junit.platform.engine.support.store.Namespace> createStore(
-			@Nullable ExtensionContext parent, LauncherStoreFacade launcherStoreFacade,
-			NamespacedHierarchicalStore.CloseAction<org.junit.platform.engine.support.store.Namespace> closeAction) {
-		NamespacedHierarchicalStore<org.junit.platform.engine.support.store.Namespace> parentStore;
-		if (parent == null) {
-			parentStore = launcherStoreFacade.getRequestLevelStore();
-		}
-		else {
-			parentStore = ((AbstractExtensionContext<?>) parent).valuesStore;
-		}
-		return new NamespacedHierarchicalStore<>(parentStore, closeAction);
-	}
-
 	@Override
 	public void close() {
 		this.valuesStore.close();


Test patch:
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ResourceAutoClosingTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ResourceAutoClosingTests.java
index 86bc5ffa08f3..f27808e18b86 100644
--- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ResourceAutoClosingTests.java
+++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ResourceAutoClosingTests.java
@@ -71,19 +71,28 @@ void shouldNotCloseAutoCloseableWhenIsClosingStoredAutoCloseablesEnabledIsFalse(
 	void shouldLogWarningWhenResourceImplementsCloseableResourceButNotAutoCloseableAndConfigIsTrue(
 			@TrackLogRecords LogRecordListener listener) throws Exception {
 		ExecutionRecorder executionRecorder = new ExecutionRecorder();
-		CloseableResource resource = new CloseableResource();
-		String msg = "Type implements CloseableResource but not AutoCloseable: " + resource.getClass().getName();
+		CloseableResource resource1 = new CloseableResource();
+		CloseableResource resource2 = new CloseableResource();
+		CloseableResource resource3 = new CloseableResource() {
+		};
 		when(configuration.isClosingStoredAutoCloseablesEnabled()).thenReturn(true);
 
 		ExtensionContext extensionContext = new JupiterEngineExtensionContext(executionRecorder, testDescriptor,
 			configuration, extensionRegistry, launcherStoreFacade);
 		ExtensionContext.Store store = extensionContext.getStore(ExtensionContext.Namespace.GLOBAL);
-		store.put("resource", resource);
+		store.put("resource1", resource1);
+		store.put("resource2", resource2);
+		store.put("resource3", resource3);
 
 		((AutoCloseable) extensionContext).close();
 
-		assertThat(listener.stream(Level.WARNING)).map(LogRecord::getMessage).contains(msg);
-		assertThat(resource.closed).isTrue();
+		assertThat(listener.stream(Level.WARNING)).map(LogRecord::getMessage) //
+				.containsExactlyInAnyOrder(
+					"Type implements CloseableResource but not AutoCloseable: " + resource1.getClass().getName(),
+					"Type implements CloseableResource but not AutoCloseable: " + resource3.getClass().getName());
+		assertThat(resource1.closed).isTrue();
+		assertThat(resource2.closed).isTrue();
+		assertThat(resource3.closed).isTrue();
 	}
 
 	@Test