Skip to content

Commit

Permalink
Allow determining exclusive resources programmatically (#3889)
Browse files Browse the repository at this point in the history
The new `@ResourceLock(providers = ...)` attribute accepts an array of
one or more classes implementing the `ResourceLocksProvider` interface.
It allows to dynamically add resources at runtime (immediately before 
starting execution on the engine level).

Resources can be distributed based on any test class or method 
attribute (e.g. package name, class / method name etc.) or any other 
custom logic.

This approach serves as a more flexible and less verbose alternative for
cases in which:
- adding lots of @ResourceLock(value, mode) annotations manually may be 
  inconvenient;
- shared resources are not known until runtime.

Resolves #2677.

---------

Co-authored-by: Marc Philipp <[email protected]>
  • Loading branch information
vdmitrienko and marcphilipp authored Oct 7, 2024
1 parent 5a49252 commit 8e9094d
Show file tree
Hide file tree
Showing 18 changed files with 1,155 additions and 35 deletions.
1 change: 1 addition & 0 deletions documentation/src/docs/asciidoc/link-attributes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ endif::[]
:Execution: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/parallel/Execution.html[@Execution]
:Isolated: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/parallel/Isolated.html[@Isolated]
:ResourceLock: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/parallel/ResourceLock.html[@ResourceLock]
:ResourceLocksProvider: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/parallel/ResourceLocksProvider.html[ResourceLocksProvider]
:Resources: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/parallel/Resources.html[Resources]
// Jupiter Extension APIs
:extension-api-package: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/package-summary.html[org.junit.jupiter.api.extension]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ JUnit repository on GitHub.
`@ConvertWith`), and `ArgumentsAggregator` (declared via `@AggregateWith`)
implementations can now use constructor injection from registered `ParameterResolver`
extensions.
* Allow determining "shared resources" at runtime via the new `@ResourceLock#providers`
attribute that accepts implementations of `ResourceLocksProvider`.


[[release-notes-5.12.0-M1-junit-vintage]]
Expand Down
15 changes: 14 additions & 1 deletion documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2943,6 +2943,12 @@ execution. The shared resource is identified by a unique name which is a `String
name can be user-defined or one of the predefined constants in `{Resources}`:
`SYSTEM_PROPERTIES`, `SYSTEM_OUT`, `SYSTEM_ERR`, `LOCALE`, or `TIME_ZONE`.

In addition to declaring these shared resources statically, the `{ResourceLock}`
annotation has a `providers` attribute that allows registering implementations of the
`{ResourceLocksProvider}` interface that can add shared resources dynamically at runtime.
Note that resources declared statically with `{ResourceLock}` annotation are combined with
resources added dynamically by `{ResourceLocksProvider}` implementations.

If the tests in the following example were run in parallel _without_ the use of
{ResourceLock}, they would be _flaky_. Sometimes they would pass, and at other times they
would fail due to the inherent race condition of writing and then reading the same JVM
Expand Down Expand Up @@ -2970,8 +2976,15 @@ parallel with each other but not while any other test that requires `READ_WRITE`
to the same shared resource is running.

[source,java]
.Declaring shared resources "statically" with `{ResourceLock}` annotation
----
include::{testDir}/example/sharedresources/StaticSharedResourcesDemo.java[tags=user_guide]
----

[source,java]
.Adding shared resources "dynamically" with `{ResourceLocksProvider}` implementation
----
include::{testDir}/example/SharedResourcesDemo.java[tags=user_guide]
include::{testDir}/example/sharedresources/DynamicSharedResourcesDemo.java[tags=user_guide]
----


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2015-2024 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package example.sharedresources;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT;
import static org.junit.jupiter.api.parallel.ResourceAccessMode.READ;
import static org.junit.jupiter.api.parallel.ResourceAccessMode.READ_WRITE;
import static org.junit.jupiter.api.parallel.Resources.SYSTEM_PROPERTIES;

import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Properties;
import java.util.Set;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ResourceAccessMode;
import org.junit.jupiter.api.parallel.ResourceLock;
import org.junit.jupiter.api.parallel.ResourceLocksProvider;

// tag::user_guide[]
@Execution(CONCURRENT)
@ResourceLock(providers = DynamicSharedResourcesDemo.Provider.class)
class DynamicSharedResourcesDemo {

private Properties backup;

@BeforeEach
void backup() {
backup = new Properties();
backup.putAll(System.getProperties());
}

@AfterEach
void restore() {
System.setProperties(backup);
}

@Test
void customPropertyIsNotSetByDefault() {
assertNull(System.getProperty("my.prop"));
}

@Test
void canSetCustomPropertyToApple() {
System.setProperty("my.prop", "apple");
assertEquals("apple", System.getProperty("my.prop"));
}

@Test
void canSetCustomPropertyToBanana() {
System.setProperty("my.prop", "banana");
assertEquals("banana", System.getProperty("my.prop"));
}

static class Provider implements ResourceLocksProvider {

@Override
public Set<Lock> provideForMethod(Class<?> testClass, Method testMethod) {
ResourceAccessMode mode = testMethod.getName().startsWith("canSet") ? READ_WRITE : READ;
return Collections.singleton(new Lock(SYSTEM_PROPERTIES, mode));
}
}

}
// end::user_guide[]
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* https://www.eclipse.org/legal/epl-v20.html
*/

package example;
package example.sharedresources;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
Expand All @@ -27,7 +27,7 @@

// tag::user_guide[]
@Execution(CONCURRENT)
class SharedResourcesDemo {
class StaticSharedResourcesDemo {

private Properties backup;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
*
* @since 5.3
* @see ResourceLock
* @see ResourceLocksProvider.Lock
*/
@API(status = STABLE, since = "5.10")
public enum ResourceAccessMode {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

package org.junit.jupiter.api.parallel;

import static org.apiguardian.api.API.Status.EXPERIMENTAL;
import static org.apiguardian.api.API.Status.STABLE;

import java.lang.annotation.ElementType;
Expand Down Expand Up @@ -48,10 +49,20 @@
* <p>Since JUnit Jupiter 5.4, this annotation is {@linkplain Inherited inherited}
* within class hierarchies.
*
* <p>Since JUnit Jupiter 5.12, this annotation supports adding shared resources
* dynamically at runtime via {@link ResourceLock#providers}.
*
* <p>Resources declared "statically" using {@link #value()} and {@link #mode()}
* are combined with "dynamic" resources added via {@link #providers()}.
* For example, declaring resource "A" via {@code @ResourceLock("A")}
* and resource "B" via a provider returning {@code new Lock("B")} will result
* in two shared resources "A" and "B".
*
* @see Isolated
* @see Resources
* @see ResourceAccessMode
* @see ResourceLocks
* @see ResourceLocksProvider
* @since 5.3
*/
@API(status = STABLE, since = "5.10")
Expand All @@ -64,17 +75,32 @@
/**
* The resource key.
*
* <p>Defaults to an empty string.
*
* @see Resources
* @see ResourceLocksProvider.Lock#getKey()
*/
String value();
String value() default "";

/**
* The resource access mode.
*
* <p>Defaults to {@link ResourceAccessMode#READ_WRITE READ_WRITE}.
*
* @see ResourceAccessMode
* @see ResourceLocksProvider.Lock#getAccessMode()
*/
ResourceAccessMode mode() default ResourceAccessMode.READ_WRITE;

/**
* An array of one or more classes implementing {@link ResourceLocksProvider}.
*
* <p>Defaults to an empty array.
*
* @see ResourceLocksProvider.Lock
* @since 5.12
*/
@API(status = EXPERIMENTAL, since = "5.12")
Class<? extends ResourceLocksProvider>[] providers() default {};

}
Loading

0 comments on commit 8e9094d

Please sign in to comment.