diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-6.0.0-M2.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-6.0.0-M2.adoc index 1e78b05dca56..ba7b708eea7d 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-6.0.0-M2.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-6.0.0-M2.adoc @@ -29,6 +29,8 @@ repository on GitHub. * `ConversionSupport` now converts `String` to `Locale` using the IETF BCP 47 language tag format supported by the `Locale.forLanguageTag(String)` factory method instead of the format used by the deprecated `Locale(String)` constructor. +* Deprecate `getOrComputeIfAbsent(...)` methods in `NamespacedHierarchicalStore` in favor + of the new `computeIfAbsent(...)` methods. [[release-notes-6.0.0-M2-junit-platform-new-features-and-improvements]] ==== New Features and Improvements @@ -51,6 +53,8 @@ repository on GitHub. * Provide cancellation support for the Suite and Vintage test engines * Introduce `TestTask.getTestDescriptor()` method for use in `HierarchicalTestExecutorService` implementations. +* Introduce `computeIfAbsent(...)` methods in `NamespacedHierarchicalStore` to simplify + working with non-nullable types. [[release-notes-6.0.0-M2-junit-jupiter]] @@ -72,12 +76,16 @@ repository on GitHub. configuration parameter. `Locale` conversions are now always performed using the IETF BCP 47 language tag format supported by the `Locale.forLanguageTag(String)` factory method. +* Deprecate `getOrComputeIfAbsent(...)` methods in `ExtensionContext.Store` in favor of + the new `computeIfAbsent(...)` methods. [[release-notes-6.0.0-M2-junit-jupiter-new-features-and-improvements]] ==== New Features and Improvements * Reason strings supplied to `ConditionEvaluationResult` APIs are now officially declared as `@Nullable`. +* Introduce `computeIfAbsent(...)` methods in `ExtensionContext.Store` to ease simplify + with non-nullable types. [[release-notes-6.0.0-M2-junit-vintage]] diff --git a/documentation/src/test/java/example/FirstCustomEngine.java b/documentation/src/test/java/example/FirstCustomEngine.java index 46c66f926540..3d84cea7370c 100644 --- a/documentation/src/test/java/example/FirstCustomEngine.java +++ b/documentation/src/test/java/example/FirstCustomEngine.java @@ -48,9 +48,6 @@ public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId return new EngineDescriptor(uniqueId, "First Custom Test Engine"); } - //end::user_guide[] - @SuppressWarnings("NullAway") - //tag::user_guide[] @Override public void execute(ExecutionRequest request) { request.getEngineExecutionListener() @@ -58,7 +55,7 @@ public void execute(ExecutionRequest request) { .executionStarted(request.getRootTestDescriptor()); NamespacedHierarchicalStore store = request.getStore(); - socket = store.getOrComputeIfAbsent(Namespace.GLOBAL, "serverSocket", key -> { + socket = store.computeIfAbsent(Namespace.GLOBAL, "serverSocket", key -> { try { return new ServerSocket(0, 50, getLoopbackAddress()); } diff --git a/documentation/src/test/java/example/SecondCustomEngine.java b/documentation/src/test/java/example/SecondCustomEngine.java index f41223d6dfcc..c6f11182f9d1 100644 --- a/documentation/src/test/java/example/SecondCustomEngine.java +++ b/documentation/src/test/java/example/SecondCustomEngine.java @@ -48,9 +48,6 @@ public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId return new EngineDescriptor(uniqueId, "Second Custom Test Engine"); } - //end::user_guide[] - @SuppressWarnings("NullAway") - //tag::user_guide[] @Override public void execute(ExecutionRequest request) { request.getEngineExecutionListener() @@ -58,7 +55,7 @@ public void execute(ExecutionRequest request) { .executionStarted(request.getRootTestDescriptor()); NamespacedHierarchicalStore store = request.getStore(); - socket = store.getOrComputeIfAbsent(Namespace.GLOBAL, "serverSocket", key -> { + socket = store.computeIfAbsent(Namespace.GLOBAL, "serverSocket", key -> { try { return new ServerSocket(0, 50, getLoopbackAddress()); } diff --git a/documentation/src/test/java/example/extensions/HttpServerExtension.java b/documentation/src/test/java/example/extensions/HttpServerExtension.java index 33a447468790..cc9947d6f060 100644 --- a/documentation/src/test/java/example/extensions/HttpServerExtension.java +++ b/documentation/src/test/java/example/extensions/HttpServerExtension.java @@ -28,16 +28,13 @@ public boolean supportsParameter(ParameterContext parameterContext, ExtensionCon return HttpServer.class.equals(parameterContext.getParameter().getType()); } - //end::user_guide[] - @SuppressWarnings({ "DataFlowIssue", "NullAway" }) - //tag::user_guide[] @Override public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { ExtensionContext rootContext = extensionContext.getRoot(); ExtensionContext.Store store = rootContext.getStore(Namespace.GLOBAL); Class key = HttpServerResource.class; - HttpServerResource resource = store.getOrComputeIfAbsent(key, __ -> { + HttpServerResource resource = store.computeIfAbsent(key, __ -> { try { HttpServerResource serverResource = new HttpServerResource(0); serverResource.start(); diff --git a/documentation/src/test/java/example/session/GlobalSetupTeardownListener.java b/documentation/src/test/java/example/session/GlobalSetupTeardownListener.java index fdddad84ea4e..bc79e04cbba8 100644 --- a/documentation/src/test/java/example/session/GlobalSetupTeardownListener.java +++ b/documentation/src/test/java/example/session/GlobalSetupTeardownListener.java @@ -43,7 +43,7 @@ public void testPlanExecutionStarted(TestPlan testPlan) { } //tag::user_guide[] NamespacedHierarchicalStore store = session.getStore(); // <1> - store.getOrComputeIfAbsent(Namespace.GLOBAL, "httpServer", key -> { // <2> + store.computeIfAbsent(Namespace.GLOBAL, "httpServer", key -> { // <2> InetSocketAddress address = new InetSocketAddress(getLoopbackAddress(), 0); HttpServer server; try { diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java index 086a89f8d37d..cc52c99f266d 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java @@ -569,10 +569,9 @@ interface CloseableResource { * * @param key the key; never {@code null} * @param requiredType the required type of the value; never {@code null} - * @param defaultValue the default value + * @param defaultValue the default value; never {@code null} * @param the value type - * @return the value; potentially {@code null} if {@code defaultValue} - * is {@code null} + * @return the value; never {@code null} * @since 5.5 * @see #get(Object, Class) */ @@ -592,13 +591,13 @@ default V getOrDefault(Object key, Class requiredType, V defaultValue) { * the type of object we wish to retrieve from the store. * *
-		 * X x = store.getOrComputeIfAbsent(X.class, key -> new X(), X.class);
+		 * X x = store.computeIfAbsent(X.class, key -> new X(), X.class);
 		 * // Equivalent to:
-		 * // X x = store.getOrComputeIfAbsent(X.class);
+		 * // X x = store.computeIfAbsent(X.class);
 		 * 
* - *

See {@link #getOrComputeIfAbsent(Object, Function, Class)} for - * further details. + *

See {@link #computeIfAbsent(Object, Function, Class)} for further + * details. * *

If {@code type} implements {@link CloseableResource} or * {@link AutoCloseable} (unless the @@ -610,14 +609,56 @@ default V getOrDefault(Object key, Class requiredType, V defaultValue) { * @param the key and value type * @return the object; never {@code null} * @since 5.1 - * @see #getOrComputeIfAbsent(Object, Function) - * @see #getOrComputeIfAbsent(Object, Function, Class) + * @see #computeIfAbsent(Class) + * @see #computeIfAbsent(Object, Function) + * @see #computeIfAbsent(Object, Function, Class) * @see CloseableResource * @see AutoCloseable + * + * @deprecated Please use {@link #computeIfAbsent(Class)} instead. */ - @API(status = STABLE, since = "5.1") - default @Nullable V getOrComputeIfAbsent(Class type) { - return getOrComputeIfAbsent(type, ReflectionSupport::newInstance, type); + @Deprecated + @API(status = DEPRECATED, since = "6.0") + default V getOrComputeIfAbsent(Class type) { + return computeIfAbsent(type); + } + + /** + * Return the object of type {@code type} if it is present and not + * {@code null} in this {@code Store} (keyed by {@code type}); + * otherwise, invoke the default constructor for {@code type} to + * generate the object, store it, and return it. + * + *

This method is a shortcut for the following, where {@code X} is + * the type of object we wish to retrieve from the store. + * + *

+		 * X x = store.computeIfAbsent(X.class, key -> new X(), X.class);
+		 * // Equivalent to:
+		 * // X x = store.computeIfAbsent(X.class);
+		 * 
+ * + *

See {@link #computeIfAbsent(Object, Function, Class)} for further + * details. + * + *

If {@code type} implements {@link CloseableResource} or + * {@link AutoCloseable} (unless the + * {@code junit.jupiter.extensions.store.close.autocloseable.enabled} + * configuration parameter is set to {@code false}), then the {@code close()} + * method will be invoked on the stored object when the store is closed. + * + * @param type the type of object to retrieve; never {@code null} + * @param the key and value type + * @return the object; never {@code null} + * @since 6.0 + * @see #computeIfAbsent(Object, Function) + * @see #computeIfAbsent(Object, Function, Class) + * @see CloseableResource + * @see AutoCloseable + */ + @API(status = MAINTAINED, since = "6.0") + default V computeIfAbsent(Class type) { + return computeIfAbsent(type, ReflectionSupport::newInstance, type); } /** @@ -631,7 +672,7 @@ default V getOrDefault(Object key, Class requiredType, V defaultValue) { * the {@code key} as input), stored, and returned. * *

For greater type safety, consider using - * {@link #getOrComputeIfAbsent(Object, Function, Class)} instead. + * {@link #computeIfAbsent(Object, Function, Class)} instead. * *

If the created value is an instance of {@link CloseableResource} or * {@link AutoCloseable} (unless the @@ -645,14 +686,56 @@ default V getOrDefault(Object key, Class requiredType, V defaultValue) { * @param the key type * @param the value type * @return the value; potentially {@code null} - * @see #getOrComputeIfAbsent(Class) - * @see #getOrComputeIfAbsent(Object, Function, Class) + * @see #computeIfAbsent(Class) + * @see #computeIfAbsent(Object, Function) + * @see #computeIfAbsent(Object, Function, Class) * @see CloseableResource * @see AutoCloseable + * + * @deprecated Please use {@link #computeIfAbsent(Object, Function)} + * instead. */ + @Deprecated + @API(status = DEPRECATED, since = "6.0") @Nullable Object getOrComputeIfAbsent(K key, Function defaultCreator); + /** + * Return the value of the specified required type that is stored under + * the supplied {@code key}. + * + *

If no value is stored in the current {@link ExtensionContext} + * for the supplied {@code key}, ancestors of the context will be queried + * for a value with the same {@code key} in the {@code Namespace} used + * to create this store. If no value is found for the supplied {@code key} + * or the value is {@code null}, a new value will be computed by the + * {@code defaultCreator} (given the {@code key} as input), stored, and + * returned. + * + *

For greater type safety, consider using + * {@link #computeIfAbsent(Object, Function, Class)} instead. + * + *

If the created value is an instance of {@link CloseableResource} or + * {@link AutoCloseable} (unless the + * {@code junit.jupiter.extensions.store.close.autocloseable.enabled} + * configuration parameter is set to {@code false}), then the {@code close()} + * method will be invoked on the stored object when the store is closed. + * + * @param key the key; never {@code null} + * @param defaultCreator the function called with the supplied {@code key} + * to create a new value; never {@code null} and must not return + * {@code null} + * @param the key type + * @param the value type + * @return the value; never {@code null} + * @since 6.0 + * @see #computeIfAbsent(Class) + * @see #computeIfAbsent(Object, Function, Class) + * @see CloseableResource + * @see AutoCloseable + */ + Object computeIfAbsent(K key, Function defaultCreator); + /** * Get the value of the specified required type that is stored under the * supplied {@code key}. @@ -664,7 +747,7 @@ default V getOrDefault(Object key, Class requiredType, V defaultValue) { * a new value will be computed by the {@code defaultCreator} (given * the {@code key} as input), stored, and returned. * - *

If {@code requiredType} implements {@link CloseableResource} or + *

If the created value implements {@link CloseableResource} or * {@link AutoCloseable} (unless the * {@code junit.jupiter.extensions.store.close.autocloseable.enabled} * configuration parameter is set to {@code false}), then the {@code close()} @@ -677,14 +760,50 @@ default V getOrDefault(Object key, Class requiredType, V defaultValue) { * @param the key type * @param the value type * @return the value; potentially {@code null} - * @see #getOrComputeIfAbsent(Class) - * @see #getOrComputeIfAbsent(Object, Function) + * @see #computeIfAbsent(Class) + * @see #computeIfAbsent(Object, Function) + * @see #computeIfAbsent(Object, Function, Class) * @see CloseableResource * @see AutoCloseable */ + @Deprecated + @API(status = DEPRECATED, since = "6.0") @Nullable V getOrComputeIfAbsent(K key, Function defaultCreator, Class requiredType); + /** + * Get the value of the specified required type that is stored under the + * supplied {@code key}. + * + *

If no value is stored in the current {@link ExtensionContext} + * for the supplied {@code key}, ancestors of the context will be queried + * for a value with the same {@code key} in the {@code Namespace} used + * to create this store. If no value is found for the supplied {@code key} + * or the value is {@code null}, a new value will be computed by the + * {@code defaultCreator} (given the {@code key} as input), stored, and + * returned. + * + *

If the created value is an instance of {@link CloseableResource} or + * {@link AutoCloseable} (unless the + * {@code junit.jupiter.extensions.store.close.autocloseable.enabled} + * configuration parameter is set to {@code false}), then the {@code close()} + * method will be invoked on the stored object when the store is closed. + * + * @param key the key; never {@code null} + * @param defaultCreator the function called with the supplied {@code key} + * to create a new value; never {@code null} and must not return + * {@code null} + * @param requiredType the required type of the value; never {@code null} + * @param the key type + * @param the value type + * @return the value; never {@code null} + * @see #computeIfAbsent(Class) + * @see #computeIfAbsent(Object, Function) + * @see CloseableResource + * @see AutoCloseable + */ + V computeIfAbsent(K key, Function defaultCreator, Class requiredType); + /** * Store a {@code value} for later retrieval under the supplied {@code key}. * diff --git a/junit-jupiter-api/src/testFixtures/java/org/junit/jupiter/api/fixtures/TrackLogRecords.java b/junit-jupiter-api/src/testFixtures/java/org/junit/jupiter/api/fixtures/TrackLogRecords.java index 0c675b07dbdd..16102f41a320 100644 --- a/junit-jupiter-api/src/testFixtures/java/org/junit/jupiter/api/fixtures/TrackLogRecords.java +++ b/junit-jupiter-api/src/testFixtures/java/org/junit/jupiter/api/fixtures/TrackLogRecords.java @@ -15,6 +15,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.jspecify.annotations.NullMarked; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtendWith; @@ -41,6 +42,7 @@ * @see LoggerFactory * @see LogRecordListener */ +@NullMarked @Target({ ElementType.TYPE, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) @ExtendWith(TrackLogRecords.Extension.class) @@ -71,7 +73,7 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte } private LogRecordListener getListener(ExtensionContext context) { - return getStore(context).getOrComputeIfAbsent(LogRecordListener.class); + return getStore(context).computeIfAbsent(LogRecordListener.class); } private Store getStore(ExtensionContext context) { diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/NamespaceAwareStore.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/NamespaceAwareStore.java index 88535ea14931..464964c9a757 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/NamespaceAwareStore.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/NamespaceAwareStore.java @@ -42,7 +42,7 @@ public NamespaceAwareStore(NamespacedHierarchicalStore valuesStore, N public @Nullable Object get(Object key) { Preconditions.notNull(key, "key must not be null"); Supplier<@Nullable Object> action = () -> this.valuesStore.get(this.namespace, key); - return accessStore(action); + return this.<@Nullable Object> accessStore(action); } @Override @@ -50,9 +50,10 @@ public NamespaceAwareStore(NamespacedHierarchicalStore valuesStore, N Preconditions.notNull(key, "key must not be null"); Preconditions.notNull(requiredType, "requiredType must not be null"); Supplier<@Nullable T> action = () -> this.valuesStore.get(this.namespace, key, requiredType); - return accessStore(action); + return this.<@Nullable T> accessStore(action); } + @SuppressWarnings("deprecation") @Override public @Nullable Object getOrComputeIfAbsent(K key, Function defaultCreator) { @@ -60,9 +61,10 @@ public NamespaceAwareStore(NamespacedHierarchicalStore valuesStore, N Preconditions.notNull(defaultCreator, "defaultCreator function must not be null"); Supplier<@Nullable Object> action = () -> this.valuesStore.getOrComputeIfAbsent(this.namespace, key, defaultCreator); - return accessStore(action); + return this.<@Nullable Object> accessStore(action); } + @SuppressWarnings("deprecation") @Override public @Nullable V getOrComputeIfAbsent(K key, Function defaultCreator, Class requiredType) { @@ -71,6 +73,23 @@ public NamespaceAwareStore(NamespacedHierarchicalStore valuesStore, N Preconditions.notNull(requiredType, "requiredType must not be null"); Supplier<@Nullable V> action = () -> this.valuesStore.getOrComputeIfAbsent(this.namespace, key, defaultCreator, requiredType); + return this.<@Nullable V> accessStore(action); + } + + @Override + public Object computeIfAbsent(K key, Function defaultCreator) { + Preconditions.notNull(key, "key must not be null"); + Preconditions.notNull(defaultCreator, "defaultCreator function must not be null"); + Supplier action = () -> this.valuesStore.computeIfAbsent(this.namespace, key, defaultCreator); + return accessStore(action); + } + + @Override + public V computeIfAbsent(K key, Function defaultCreator, Class requiredType) { + Preconditions.notNull(key, "key must not be null"); + Preconditions.notNull(defaultCreator, "defaultCreator function must not be null"); + Preconditions.notNull(requiredType, "requiredType must not be null"); + Supplier action = () -> this.valuesStore.computeIfAbsent(this.namespace, key, defaultCreator, requiredType); return accessStore(action); } @@ -78,14 +97,14 @@ public NamespaceAwareStore(NamespacedHierarchicalStore valuesStore, N public void put(Object key, @Nullable Object value) { Preconditions.notNull(key, "key must not be null"); Supplier<@Nullable Object> action = () -> this.valuesStore.put(this.namespace, key, value); - accessStore(action); + this.<@Nullable Object> accessStore(action); } @Override public @Nullable Object remove(Object key) { Preconditions.notNull(key, "key must not be null"); Supplier<@Nullable Object> action = () -> this.valuesStore.remove(this.namespace, key); - return accessStore(action); + return this.<@Nullable Object> accessStore(action); } @Override @@ -93,10 +112,10 @@ public void put(Object key, @Nullable Object value) { Preconditions.notNull(key, "key must not be null"); Preconditions.notNull(requiredType, "requiredType must not be null"); Supplier<@Nullable T> action = () -> this.valuesStore.remove(this.namespace, key, requiredType); - return accessStore(action); + return this.<@Nullable T> accessStore(action); } - private @Nullable T accessStore(Supplier<@Nullable T> action) { + private T accessStore(Supplier action) { try { return action.get(); } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TempDirectory.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TempDirectory.java index 954099cbf1da..f189973cd213 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TempDirectory.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TempDirectory.java @@ -12,7 +12,6 @@ import static java.nio.file.FileVisitResult.CONTINUE; import static java.nio.file.FileVisitResult.SKIP_SUBTREE; -import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.joining; import static org.junit.jupiter.api.extension.TestInstantiationAwareExtension.ExtensionContextScope.TEST_METHOD; import static org.junit.jupiter.api.io.CleanupMode.DEFAULT; @@ -242,11 +241,11 @@ private static void assertSupportedType(String target, Class type) { private static Object getPathOrFile(Class elementType, AnnotatedElementContext elementContext, TempDirFactory factory, CleanupMode cleanupMode, ExtensionContext extensionContext) { - Path path = requireNonNull(extensionContext.getStore(NAMESPACE.append(elementContext)) // - .getOrComputeIfAbsent(KEY, + Path path = extensionContext.getStore(NAMESPACE.append(elementContext)) // + .computeIfAbsent(KEY, __ -> createTempDir(factory, cleanupMode, elementType, elementContext, extensionContext), - CloseablePath.class)) // - .get(); + CloseablePath.class) // + .get(); return (elementType == Path.class) ? path : path.toFile(); } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TimeoutExtension.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TimeoutExtension.java index 4c8a611f3eed..7d50cc928a1d 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TimeoutExtension.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TimeoutExtension.java @@ -10,7 +10,6 @@ package org.junit.jupiter.engine.extension; -import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Timeout.TIMEOUT_MODE_PROPERTY_NAME; import static org.junit.jupiter.api.Timeout.ThreadMode.SAME_THREAD; @@ -172,8 +171,8 @@ private Optional readTimeoutThreadModeFromAnnotation(Optional new TimeoutConfiguration(root), TimeoutConfiguration.class)); + return root.getStore(NAMESPACE).computeIfAbsent(GLOBAL_TIMEOUT_CONFIG_KEY, + key -> new TimeoutConfiguration(root), TimeoutConfiguration.class); } private Invocation decorate(Invocation invocation, diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactory.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactory.java index daa4853550ef..2c066ba9e945 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactory.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactory.java @@ -10,8 +10,6 @@ package org.junit.jupiter.engine.extension; -import static java.util.Objects.requireNonNull; - import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -50,7 +48,7 @@ Invocation create(ThreadMode threadMode, TimeoutInvocationParameters t } private ScheduledExecutorService getThreadExecutorForSameThreadInvocation() { - return requireNonNull(store.getOrComputeIfAbsent(SingleThreadExecutorResource.class)).get(); + return store.computeIfAbsent(SingleThreadExecutorResource.class).get(); } @SuppressWarnings({ "deprecation", "try" }) diff --git a/junit-jupiter-migrationsupport/src/main/java/org/junit/jupiter/migrationsupport/rules/ExpectedExceptionSupport.java b/junit-jupiter-migrationsupport/src/main/java/org/junit/jupiter/migrationsupport/rules/ExpectedExceptionSupport.java index e4a7e4e9bb0a..e909745b4f41 100644 --- a/junit-jupiter-migrationsupport/src/main/java/org/junit/jupiter/migrationsupport/rules/ExpectedExceptionSupport.java +++ b/junit-jupiter-migrationsupport/src/main/java/org/junit/jupiter/migrationsupport/rules/ExpectedExceptionSupport.java @@ -61,8 +61,8 @@ public void handleTestExecutionException(ExtensionContext context, Throwable thr @Override public void afterEach(ExtensionContext context) throws Exception { - Boolean handled = getStore(context).getOrComputeIfAbsent(EXCEPTION_WAS_HANDLED, key -> false, Boolean.class); - if (handled != null && !handled) { + boolean handled = getStore(context).computeIfAbsent(EXCEPTION_WAS_HANDLED, key -> false, Boolean.class); + if (!handled) { this.support.afterEach(context); } } diff --git a/junit-jupiter-migrationsupport/src/main/java/org/junit/jupiter/migrationsupport/rules/TestRuleSupport.java b/junit-jupiter-migrationsupport/src/main/java/org/junit/jupiter/migrationsupport/rules/TestRuleSupport.java index c13ac33b1985..0b4c286c1e62 100644 --- a/junit-jupiter-migrationsupport/src/main/java/org/junit/jupiter/migrationsupport/rules/TestRuleSupport.java +++ b/junit-jupiter-migrationsupport/src/main/java/org/junit/jupiter/migrationsupport/rules/TestRuleSupport.java @@ -11,7 +11,6 @@ package org.junit.jupiter.migrationsupport.rules; import static java.util.Collections.unmodifiableList; -import static java.util.Objects.requireNonNull; import static org.junit.platform.commons.support.AnnotationSupport.findPublicAnnotatedFields; import static org.junit.platform.commons.support.AnnotationSupport.isAnnotated; import static org.junit.platform.commons.support.HierarchyTraversalMode.TOP_DOWN; @@ -147,8 +146,8 @@ private List getRuleAnnotatedMembers(ExtensionContext c Object testInstance = context.getRequiredTestInstance(); Namespace namespace = Namespace.create(TestRuleSupport.class, context.getRequiredTestClass()); // @formatter:off - return new ArrayList<>(requireNonNull(context.getStore(namespace) - .getOrComputeIfAbsent("rule-annotated-members", key -> findRuleAnnotatedMembers(testInstance), List.class))); + return new ArrayList<>(context.getStore(namespace) + .computeIfAbsent("rule-annotated-members", key -> findRuleAnnotatedMembers(testInstance), List.class)); // @formatter:on } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidator.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidator.java index 0be381158c6a..eabcb067044f 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidator.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidator.java @@ -10,8 +10,6 @@ package org.junit.jupiter.params; -import static java.util.Objects.requireNonNull; - import java.util.Arrays; import java.util.Optional; @@ -72,7 +70,7 @@ private ArgumentCountValidationMode getArgumentCountValidationModeConfiguration( String key = ARGUMENT_COUNT_VALIDATION_KEY; ArgumentCountValidationMode fallback = ArgumentCountValidationMode.NONE; ExtensionContext.Store store = getStore(extensionContext); - return requireNonNull(store.getOrComputeIfAbsent(key, __ -> { + return store.computeIfAbsent(key, __ -> { Optional optionalConfigValue = extensionContext.getConfigurationParameter(key); if (optionalConfigValue.isPresent()) { String configValue = optionalConfigValue.get(); @@ -95,7 +93,7 @@ private ArgumentCountValidationMode getArgumentCountValidationModeConfiguration( else { return fallback; } - }, ArgumentCountValidationMode.class)); + }, ArgumentCountValidationMode.class); } private static String pluralize(int count, String singular, String plural) { diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.java index 72826970014a..5e9c3157ffee 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.java @@ -12,6 +12,7 @@ import static java.util.Comparator.comparing; import static java.util.Objects.requireNonNull; +import static org.apiguardian.api.API.Status.DEPRECATED; import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.MAINTAINED; import static org.junit.platform.commons.util.ReflectionUtils.getWrapperType; @@ -188,7 +189,12 @@ public void close() { * @return the stored value; may be {@code null} * @throws NamespacedHierarchicalStoreException if this store has already been * closed + * + * @deprecated Please use + * {@link #computeIfAbsent(Object, Object, Function)} instead. */ + @Deprecated + @API(status = DEPRECATED, since = "6.0") public @Nullable Object getOrComputeIfAbsent(N namespace, K key, Function defaultCreator) { Preconditions.notNull(defaultCreator, "defaultCreator must not be null"); @@ -204,6 +210,45 @@ public void close() { return storedValue.evaluate(); } + /** + * Return the value stored for the supplied namespace and key in this store + * or the parent store, if present and not {@code null}, or call the + * supplied function to compute it. + * + * @param namespace the namespace; never {@code null} + * @param key the key; never {@code null} + * @param defaultCreator the function called with the supplied {@code key} + * to create a new value; never {@code null} and must not return + * {@code null} + * @return the stored value; never {@code null} + * @throws NamespacedHierarchicalStoreException if this store has already been + * closed + * @since 6.0 + */ + @API(status = MAINTAINED, since = "6.0") + public Object computeIfAbsent(N namespace, K key, Function defaultCreator) { + Preconditions.notNull(defaultCreator, "defaultCreator must not be null"); + CompositeKey compositeKey = new CompositeKey<>(namespace, key); + StoredValue storedValue = getStoredValue(compositeKey); + var result = StoredValue.evaluateIfNotNull(storedValue); + if (result == null) { + StoredValue newStoredValue = this.storedValues.compute(compositeKey, (__, oldStoredValue) -> { + if (StoredValue.evaluateIfNotNull(oldStoredValue) == null) { + rejectIfClosed(); + var computedValue = Preconditions.notNull(defaultCreator.apply(key), + "defaultCreator must not return null"); + return newStoredValue(() -> { + rejectIfClosed(); + return computedValue; + }); + } + return oldStoredValue; + }); + return requireNonNull(newStoredValue.evaluate()); + } + return result; + } + /** * Get the value stored for the supplied namespace and key in this store or * the parent store, if present, or call the supplied function to compute it @@ -217,7 +262,12 @@ public void close() { * @return the stored value; may be {@code null} * @throws NamespacedHierarchicalStoreException if the stored value cannot * be cast to the required type, or if this store has already been closed + * + * @deprecated Please use + * {@link #computeIfAbsent(Object, Object, Function, Class)} instead. */ + @Deprecated + @API(status = DEPRECATED, since = "6.0") public @Nullable V getOrComputeIfAbsent(N namespace, K key, Function defaultCreator, Class requiredType) throws NamespacedHierarchicalStoreException { @@ -226,6 +276,31 @@ public void close() { return castToRequiredType(key, value, requiredType); } + /** + * Return the value stored for the supplied namespace and key in this store + * or the parent store, if present and not {@code null}, or call the + * supplied function to compute it and, finally, cast it to the supplied + * required type. + * + * @param namespace the namespace; never {@code null} + * @param key the key; never {@code null} + * @param defaultCreator the function called with the supplied {@code key} + * to create a new value; never {@code null} and must not return + * {@code null} + * @param requiredType the required type of the value; never {@code null} + * @return the stored value; never {@code null} + * @throws NamespacedHierarchicalStoreException if the stored value cannot + * be cast to the required type, or if this store has already been closed + * @since 6.0 + */ + @API(status = MAINTAINED, since = "6.0") + public V computeIfAbsent(N namespace, K key, Function defaultCreator, + Class requiredType) throws NamespacedHierarchicalStoreException { + + Object value = computeIfAbsent(namespace, key, defaultCreator); + return castNonNullToRequiredType(key, value, requiredType); + } + /** * Put the supplied value for the supplied namespace and key into this * store and return the previously associated value in this store. @@ -302,12 +377,16 @@ private StoredValue newStoredValue(Supplier<@Nullable Object> value) { return null; } - @SuppressWarnings("unchecked") private @Nullable T castToRequiredType(Object key, @Nullable Object value, Class requiredType) { Preconditions.notNull(requiredType, "requiredType must not be null"); if (value == null) { return null; } + return castNonNullToRequiredType(key, value, requiredType); + } + + @SuppressWarnings("unchecked") + private T castNonNullToRequiredType(Object key, V value, Class requiredType) { if (isAssignableTo(value, requiredType)) { if (requiredType.isPrimitive()) { return (T) requireNonNull(getWrapperType(requiredType)).cast(value); diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/Heavyweight.java b/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/Heavyweight.java index a7552d2787f2..6314bef04ef9 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/Heavyweight.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/Heavyweight.java @@ -10,7 +10,6 @@ package org.junit.jupiter.api.extension; -import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -33,7 +32,7 @@ public boolean supportsParameter(ParameterContext parameterContext, ExtensionCon public Object resolveParameter(ParameterContext parameterContext, ExtensionContext context) { var engineContext = context.getRoot(); var store = engineContext.getStore(ExtensionContext.Namespace.GLOBAL); - var resource = requireNonNull(store.getOrComputeIfAbsent(ResourceValue.class)); + var resource = store.computeIfAbsent(ResourceValue.class); resource.usages.incrementAndGet(); return resource; } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java index 2599244c739c..61993435a692 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java @@ -409,7 +409,7 @@ private ExtensionContext createExtensionContextForFilePublishing(Path tempDir, } @Test - @SuppressWarnings("resource") + @SuppressWarnings({ "resource", "deprecation" }) void usingStore() { var methodTestDescriptor = methodDescriptor(); var classTestDescriptor = outerClassDescriptor(methodTestDescriptor); @@ -436,14 +436,17 @@ void usingStore() { final Object key2 = "key 2"; final var value2 = "other value"; - assertEquals(value2, childStore.getOrComputeIfAbsent(key2, key -> value2)); - assertEquals(value2, childStore.getOrComputeIfAbsent(key2, key -> value2, String.class)); + assertEquals(value2, childStore.computeIfAbsent(key2, key -> value2)); + assertEquals(value2, childStore.computeIfAbsent(key2, key -> "a different value", String.class)); + assertEquals(value2, childStore.getOrComputeIfAbsent(key2, key -> "a different value")); + assertEquals(value2, childStore.getOrComputeIfAbsent(key2, key -> "a different value", String.class)); assertEquals(value2, childStore.get(key2)); assertEquals(value2, childStore.get(key2, String.class)); final Object parentKey = "parent key"; final var parentValue = "parent value"; parentStore.put(parentKey, parentValue); + assertEquals(parentValue, childStore.computeIfAbsent(parentKey, k -> "a different value")); assertEquals(parentValue, childStore.getOrComputeIfAbsent(parentKey, k -> "a different value")); assertEquals(parentValue, childStore.get(parentKey)); } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreConcurrencyTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreConcurrencyTests.java index c39cc37aae7c..b93929f80b51 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreConcurrencyTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreConcurrencyTests.java @@ -35,7 +35,7 @@ void concurrentAccessToDefaultStoreWithoutParentStore() { IntStream.range(1, 100).forEach(i -> { Store store = reset(); // Simulate 100 extensions interacting concurrently with the Store. - IntStream.range(1, 100).parallel().forEach(j -> store.getOrComputeIfAbsent("key", this::newValue)); + IntStream.range(1, 100).parallel().forEach(j -> store.computeIfAbsent("key", this::newValue)); assertEquals(1, count.get(), () -> "number of times newValue() was invoked in run #" + i); }); } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreTests.java index 849d481212c6..d90c2ed0f88c 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreTests.java @@ -12,6 +12,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -77,6 +78,7 @@ void getOrDefaultWithValueInParentStore() { assertThat(store.getOrDefault(KEY, String.class, VALUE)).isEqualTo(VALUE); } + @SuppressWarnings("deprecation") @Test void getOrComputeIfAbsentWithFailingCreator() { var invocations = new AtomicInteger(); @@ -92,4 +94,13 @@ void getOrComputeIfAbsentWithFailingCreator() { assertThat(invocations).hasValue(1); } + @Test + void computeIfAbsentWithFailingCreator() { + assertThrows(RuntimeException.class, () -> store.computeIfAbsent(KEY, __ -> { + throw new RuntimeException(); + })); + assertNull(store.get(KEY)); + assertDoesNotThrow(localStore::close); + } + } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/ExtensionContextExecutionTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/ExtensionContextExecutionTests.java index 5f3281917664..9cf00441de43 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/ExtensionContextExecutionTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/ExtensionContextExecutionTests.java @@ -73,7 +73,7 @@ static class OnlyIncrementCounterOnce implements BeforeAllCallback { @Override public void beforeAll(ExtensionContext context) { ExtensionContext.Store store = getRoot(context).getStore(ExtensionContext.Namespace.GLOBAL); - store.getOrComputeIfAbsent("counter", key -> Parent.counter.incrementAndGet()); + store.computeIfAbsent("counter", key -> Parent.counter.incrementAndGet()); } private ExtensionContext getRoot(ExtensionContext context) { diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactoryTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactoryTests.java index 7e6e6e4f0d2a..946a0344179e 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactoryTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactoryTests.java @@ -85,12 +85,13 @@ void shouldThrowExceptionWhenNullTimeoutInvocationParametersIsProvidedWhenCreate .hasMessage("timeout invocation parameters must not be null"); } + @SuppressWarnings("resource") @Test @DisplayName("creates timeout invocation for SAME_THREAD thread mode") void shouldCreateTimeoutInvocationForSameThreadTimeoutThreadMode() { var invocation = timeoutInvocationFactory.create(ThreadMode.SAME_THREAD, parameters); assertThat(invocation).isInstanceOf(SameThreadTimeoutInvocation.class); - verify(store).getOrComputeIfAbsent(SingleThreadExecutorResource.class); + verify(store).computeIfAbsent(SingleThreadExecutorResource.class); } @Test diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStoreTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStoreTests.java index 0cc43881b122..571f321f6d84 100644 --- a/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStoreTests.java +++ b/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStoreTests.java @@ -78,50 +78,67 @@ void valueCanBeReplaced() { @Test void valueIsComputedIfAbsent() { assertNull(store.get(namespace, key)); - assertEquals(value, store.getOrComputeIfAbsent(namespace, key, innerKey -> value)); + assertEquals(value, store.computeIfAbsent(namespace, key, __ -> value)); assertEquals(value, store.get(namespace, key)); } + @SuppressWarnings("deprecation") @Test void valueIsNotComputedIfPresentLocally() { store.put(namespace, key, value); - assertEquals(value, store.getOrComputeIfAbsent(namespace, key, innerKey -> "a different value")); + assertEquals(value, store.getOrComputeIfAbsent(namespace, key, __ -> "a different value")); + assertEquals(value, store.computeIfAbsent(namespace, key, __ -> "a different value")); assertEquals(value, store.get(namespace, key)); } + @SuppressWarnings("deprecation") @Test void valueIsNotComputedIfPresentInParent() { parentStore.put(namespace, key, value); - assertEquals(value, store.getOrComputeIfAbsent(namespace, key, k -> "a different value")); + assertEquals(value, store.getOrComputeIfAbsent(namespace, key, __ -> "a different value")); + assertEquals(value, store.computeIfAbsent(namespace, key, __ -> "a different value")); assertEquals(value, store.get(namespace, key)); } + @SuppressWarnings("deprecation") @Test void valueIsNotComputedIfPresentInGrandParent() { grandParentStore.put(namespace, key, value); - assertEquals(value, store.getOrComputeIfAbsent(namespace, key, k -> "a different value")); + assertEquals(value, store.getOrComputeIfAbsent(namespace, key, __ -> "a different value")); + assertEquals(value, store.computeIfAbsent(namespace, key, __ -> "a different value")); assertEquals(value, store.get(namespace, key)); } + @SuppressWarnings("deprecation") @Test void nullIsAValidValueToPut() { store.put(namespace, key, null); - assertNull(store.getOrComputeIfAbsent(namespace, key, innerKey -> "a different value")); + assertNull(store.getOrComputeIfAbsent(namespace, key, __ -> "a different value")); assertNull(store.get(namespace, key)); + + assertEquals("a different value", store.computeIfAbsent(namespace, key, __ -> "a different value")); + assertEquals("a different value", store.get(namespace, key)); } + @SuppressWarnings("deprecation") @Test void keysCanBeRemoved() { store.put(namespace, key, value); assertEquals(value, store.remove(namespace, key)); + assertNull(store.get(namespace, key)); + + assertEquals("a different value", store.getOrComputeIfAbsent(namespace, key, __ -> "a different value")); + assertEquals("a different value", store.remove(namespace, key)); + assertNull(store.get(namespace, key)); + assertEquals("another different value", + store.computeIfAbsent(namespace, key, __ -> "another different value")); + assertEquals("another different value", store.remove(namespace, key)); assertNull(store.get(namespace, key)); - assertEquals("a different value", - store.getOrComputeIfAbsent(namespace, key, innerKey -> "a different value")); } @Test @@ -144,7 +161,7 @@ void valueIsComputedIfAbsentInDifferentNamespace() { String namespace1 = "ns1"; String namespace2 = "ns2"; - assertEquals(value, store.getOrComputeIfAbsent(namespace1, key, innerKey -> value)); + assertEquals(value, store.computeIfAbsent(namespace1, key, __ -> value)); assertEquals(value, store.get(namespace1, key)); assertNull(store.get(namespace2, key)); @@ -213,6 +230,7 @@ void getNullValueWithTypeSafety() { assertNull(requiredTypeValue); } + @SuppressWarnings("deprecation") @Test void getOrComputeIfAbsentWithTypeSafetyAndInvalidRequiredTypeThrowsException() { String key = "pi"; @@ -231,6 +249,25 @@ void getOrComputeIfAbsentWithTypeSafetyAndInvalidRequiredTypeThrowsException() { exception.getMessage()); } + @Test + void computeIfAbsentWithTypeSafetyAndInvalidRequiredTypeThrowsException() { + String key = "pi"; + Float value = 3.14f; + + // Store a Float... + store.put(namespace, key, value); + + // But declare that our function creates a String... + Function defaultCreator = k -> "enigma"; + + Exception exception = assertThrows(NamespacedHierarchicalStoreException.class, + () -> store.computeIfAbsent(namespace, key, defaultCreator, String.class)); + assertEquals( + "Object stored under key [pi] is not of required type [java.lang.String], but was [java.lang.Float]: 3.14", + exception.getMessage()); + } + + @SuppressWarnings("deprecation") @Test void getOrComputeIfAbsentWithTypeSafety() { Integer key = 42; @@ -241,7 +278,17 @@ void getOrComputeIfAbsentWithTypeSafety() { assertEquals(value, computedValue); } - @SuppressWarnings({ "DataFlowIssue", "NullAway" }) + @Test + void computeIfAbsentWithTypeSafety() { + Integer key = 42; + String value = "enigma"; + + // The fact that we can declare this as a String suffices for testing the required type. + String computedValue = store.computeIfAbsent(namespace, key, __ -> value, String.class); + assertEquals(value, computedValue); + } + + @SuppressWarnings({ "DataFlowIssue", "NullAway", "deprecation" }) @Test void getOrComputeIfAbsentWithTypeSafetyAndPrimitiveValueType() { String key = "enigma"; @@ -254,6 +301,19 @@ void getOrComputeIfAbsentWithTypeSafetyAndPrimitiveValueType() { assertEquals(value, computedInteger.intValue()); } + @Test + void computeIfAbsentWithTypeSafetyAndPrimitiveValueType() { + String key = "enigma"; + int value = 42; + + // The fact that we can declare this as an int/Integer suffices for testing the required type. + int computedInt = store.computeIfAbsent(namespace, key, k -> value, int.class); + Integer computedInteger = store.computeIfAbsent(namespace, key, k -> value, Integer.class); + assertEquals(value, computedInt); + assertEquals(value, computedInteger.intValue()); + } + + @SuppressWarnings("deprecation") @Test void getOrComputeIfAbsentWithExceptionThrowingCreatorFunction() { var e = assertThrows(RuntimeException.class, () -> store.getOrComputeIfAbsent(namespace, key, __ -> { @@ -263,6 +323,15 @@ void getOrComputeIfAbsentWithExceptionThrowingCreatorFunction() { assertSame(e, assertThrows(RuntimeException.class, () -> store.remove(namespace, key))); } + @Test + void computeIfAbsentWithExceptionThrowingCreatorFunction() { + assertThrows(RuntimeException.class, () -> store.computeIfAbsent(namespace, key, __ -> { + throw new RuntimeException("boom"); + })); + assertNull(store.get(namespace, key)); + assertNull(store.remove(namespace, key)); + } + @Test void removeWithTypeSafetyAndInvalidRequiredTypeThrowsException() { Integer key = 42; @@ -316,6 +385,7 @@ void removeNullValueWithTypeSafety() { assertNull(store.get(namespace, key)); } + @SuppressWarnings("deprecation") @Test void simulateRaceConditionInGetOrComputeIfAbsent() throws Exception { int threads = 10; @@ -331,6 +401,21 @@ void simulateRaceConditionInGetOrComputeIfAbsent() throws Exception { assertEquals(1, counter.get()); assertThat(values).hasSize(threads).containsOnly(1); } + + @Test + void simulateRaceConditionInComputeIfAbsent() throws Exception { + int threads = 10; + AtomicInteger counter = new AtomicInteger(); + List values; + + try (var localStore = new NamespacedHierarchicalStore<>(null)) { + values = executeConcurrently(threads, // + () -> requireNonNull(localStore.computeIfAbsent(namespace, key, it -> counter.incrementAndGet()))); + } + + assertEquals(1, counter.get()); + assertThat(values).hasSize(threads).containsOnly(1); + } } @Nested @@ -433,9 +518,13 @@ void doesNotCallCloseActionForNullValues() { verifyNoInteractions(closeAction); } + @SuppressWarnings("deprecation") @Test void doesNotCallCloseActionForValuesThatThrowExceptionsDuringCleanup() throws Throwable { store.put(namespace, "key1", "value1"); + assertThrows(RuntimeException.class, () -> store.computeIfAbsent(namespace, "key2", __ -> { + throw new RuntimeException("boom"); + })); assertThrows(RuntimeException.class, () -> store.getOrComputeIfAbsent(namespace, "key2", __ -> { throw new RuntimeException("boom"); })); @@ -447,10 +536,10 @@ void doesNotCallCloseActionForValuesThatThrowExceptionsDuringCleanup() throws Th var inOrder = inOrder(closeAction); inOrder.verify(closeAction).close(namespace, "key3", "value3"); inOrder.verify(closeAction).close(namespace, "key1", "value1"); - - verifyNoMoreInteractions(closeAction); + inOrder.verifyNoMoreInteractions(); } + @SuppressWarnings("deprecation") @Test void abortsCloseIfAnyStoredValueThrowsAnUnrecoverableExceptionDuringCleanup() throws Throwable { store.put(namespace, "key1", "value1"); @@ -527,6 +616,7 @@ void closeIsIdempotent() throws Throwable { /** * @see #3944 */ + @SuppressWarnings("deprecation") @Test void acceptsQueryAfterClose() { store.put(namespace, key, value); @@ -535,10 +625,13 @@ void acceptsQueryAfterClose() { assertThat(store.get(namespace, key)).isEqualTo(value); assertThat(store.get(namespace, key, String.class)).isEqualTo(value); - assertThat(store.getOrComputeIfAbsent(namespace, key, k -> "new")).isEqualTo(value); - assertThat(store.getOrComputeIfAbsent(namespace, key, k -> "new", String.class)).isEqualTo(value); + assertThat(store.getOrComputeIfAbsent(namespace, key, __ -> "new")).isEqualTo(value); + assertThat(store.getOrComputeIfAbsent(namespace, key, __ -> "new", String.class)).isEqualTo(value); + assertThat(store.computeIfAbsent(namespace, key, __ -> "new")).isEqualTo(value); + assertThat(store.computeIfAbsent(namespace, key, __ -> "new", String.class)).isEqualTo(value); } + @SuppressWarnings("deprecation") @Test void rejectsModificationAfterClose() { store.close(); @@ -548,11 +641,16 @@ void rejectsModificationAfterClose() { assertThrows(NamespacedHierarchicalStoreException.class, () -> store.remove(namespace, key)); assertThrows(NamespacedHierarchicalStoreException.class, () -> store.remove(namespace, key, int.class)); - // Since key does not exist, an invocation of getOrComputeIfAbsent(...) will attempt to compute a new value. + // Since key does not exist, an invocation of getOrComputeIfAbsent(...) or computeIfAbsent(...) will attempt + // to compute a new value. + assertThrows(NamespacedHierarchicalStoreException.class, + () -> store.getOrComputeIfAbsent(namespace, key, __ -> "new")); + assertThrows(NamespacedHierarchicalStoreException.class, + () -> store.getOrComputeIfAbsent(namespace, key, __ -> "new", String.class)); assertThrows(NamespacedHierarchicalStoreException.class, - () -> store.getOrComputeIfAbsent(namespace, key, k -> "new")); + () -> store.computeIfAbsent(namespace, key, __ -> "new")); assertThrows(NamespacedHierarchicalStoreException.class, - () -> store.getOrComputeIfAbsent(namespace, key, k -> "new", String.class)); + () -> store.computeIfAbsent(namespace, key, __ -> "new", String.class)); } private void assertNotClosed() { diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/LocalMavenRepo.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/LocalMavenRepo.java index cb3eb217fa75..50d1f3aac0ad 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/LocalMavenRepo.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/LocalMavenRepo.java @@ -32,7 +32,7 @@ public static class ScopeProvider implements ManagedResource.Scoped.Provider { @Override public ManagedResource.Scope determineScope(ExtensionContext extensionContext) { var store = extensionContext.getRoot().getStore(NAMESPACE); - var fileSystemType = store.getOrComputeIfAbsent("tempFileSystemType", key -> { + var fileSystemType = store.computeIfAbsent("tempFileSystemType", key -> { var type = getFileSystemType(Path.of(System.getProperty("java.io.tmpdir"))); extensionContext.getRoot().publishReportEntry("tempFileSystemType", type); return type; diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ManagedResource.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ManagedResource.java index ce872f8f0f95..3324fce2714b 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ManagedResource.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ManagedResource.java @@ -95,7 +95,7 @@ private Resource getOrCreateResource(ExtensionContext extensionContext, C case PER_CONTEXT -> extensionContext; }; return storingContext.getStore(Namespace.GLOBAL) // - .getOrComputeIfAbsent(type, Resource::new, Resource.class); + .computeIfAbsent(type, Resource::new, Resource.class); } } diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/OutputAttachingExtension.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/OutputAttachingExtension.java index 9621414e9d4c..c9d84b2c7767 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/OutputAttachingExtension.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/OutputAttachingExtension.java @@ -40,7 +40,7 @@ public boolean supportsParameter(ParameterContext parameterContext, ExtensionCon @Override public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { - var outputDir = extensionContext.getStore(NAMESPACE).getOrComputeIfAbsent("outputDir", __ -> { + var outputDir = extensionContext.getStore(NAMESPACE).computeIfAbsent("outputDir", __ -> { try { return new OutputDir(Files.createTempDirectory("output")); }