Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support programmatic registration of resource locks #3889

Merged
merged 30 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7fc2b96
Introduce adding 'resource locks' programmatically.
vdmitrienko Jul 31, 2024
cde4bc8
Add 'ResourceLocksProviderIntegrationTests'.
vdmitrienko Aug 1, 2024
00788b6
Add JavaDoc.
vdmitrienko Aug 6, 2024
ff3c786
Add 'locks from provider' cases to 'NodeTreeWalkerIntegrationTests'.
vdmitrienko Aug 6, 2024
1c6eef8
Add 'LockTests'.
vdmitrienko Aug 6, 2024
7d894c5
Add user guide.
vdmitrienko Aug 7, 2024
1f8a6d0
Revert "Add 'locks from provider' cases to 'NodeTreeWalkerIntegration…
vdmitrienko Aug 12, 2024
e1b5473
Move '@ResourceLocksFrom' functionality to '@ResourceLock'.
vdmitrienko Aug 12, 2024
4460685
Add tests for '@ResourceLock' and 'ResourceLocksProvider'.
vdmitrienko Aug 15, 2024
e14bfda
Merge branch 'main' of https://github.com/VladimirDmitrienko/junit5 i…
vdmitrienko Aug 15, 2024
ba476e5
Fix formatting.
vdmitrienko Aug 15, 2024
1d8488c
Polish Javadoc
marcphilipp Sep 18, 2024
3fddd55
Merge branch 'refs/heads/main' into #2677_add_exclusive_resource_prog…
vdmitrienko Sep 20, 2024
49f9a28
Merge branch 'refs/heads/main' into #2677_add_exclusive_resource_prog…
vdmitrienko Sep 25, 2024
d56196d
Remove version from User Guide.
vdmitrienko Sep 25, 2024
c8d26ef
Revert formatting changes in User Guide.
vdmitrienko Sep 25, 2024
9383c39
Mention that `ResourceLocksProvider` must have a no-args constructor.
vdmitrienko Sep 27, 2024
249ab3e
Avoid searching for the annotation twice.
vdmitrienko Sep 27, 2024
67e7dc5
Polish `DynamicSharedResourcesDemo`.
vdmitrienko Sep 27, 2024
792b43a
Add Release Notes.
vdmitrienko Sep 27, 2024
ef2f38f
Collect locks from the providers of the enclosing classes.
vdmitrienko Oct 3, 2024
730a99d
Merge branch 'refs/heads/main' into #2677_add_exclusive_resource_prog…
vdmitrienko Oct 4, 2024
cb01e24
Fix formatting.
vdmitrienko Oct 4, 2024
35acb01
Fix formatting.
vdmitrienko Oct 4, 2024
3d8af62
Remove debug code.
vdmitrienko Oct 4, 2024
780c03b
Test that the locks from the 'outer' and 'nested' providers combined.
vdmitrienko Oct 4, 2024
fed909b
Apply suggestions from code review
marcphilipp Oct 7, 2024
826fb0f
Polish Javadoc
marcphilipp Oct 7, 2024
dc6b2a9
Remove unused imports
marcphilipp Oct 7, 2024
a728b5b
Polish code snippet
marcphilipp Oct 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
42 changes: 29 additions & 13 deletions documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2927,21 +2927,30 @@ In addition to controlling the execution mode using the `{Execution}` annotation
Jupiter provides another annotation-based declarative synchronization mechanism. The
`{ResourceLock}` annotation allows you to declare that a test class or method uses a
specific shared resource that requires synchronized access to ensure reliable test
execution. The shared resource is identified by a unique name which is a `String`. The
name can be user-defined or one of the predefined constants in `{Resources}`:
execution. The shared resource is identified by a unique name which is a `String`.
The name can be user-defined or one of the predefined constants in `{Resources}`:
`SYSTEM_PROPERTIES`, `SYSTEM_OUT`, `SYSTEM_ERR`, `LOCALE`, or `TIME_ZONE`.

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
System Property.
Since Junit Jupiter 5.12 `{ResourceLock}` annotation has 'providers' attribute
marcphilipp marked this conversation as resolved.
Show resolved Hide resolved
which accepts an array of one or more classes implementing `{ResourceLocksProvider}`
interface. This interface allows to add shared resources in runtime.

When access to shared resources is declared using the `{ResourceLock}` annotation, the
JUnit Jupiter engine uses this information to ensure that no conflicting tests are run in
parallel. This guarantee extends to lifecycle methods of a test class or method. For
example, if a test method is annotated with a `{ResourceLock}` annotation, the "lock" will
be acquired before any `@BeforeEach` methods are executed and released after all
`@AfterEach` methods have been executed.
Note that resources declared "statically" with `{ResourceLock}` annotation
are combined with resources added "dynamically" with `{ResourceLocksProvider}`
interface.

If the tests in the following examples were run in parallel _without_ the use of
`{ResourceLock}` or `{ResourceLocksProvider}`, 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 System Property.

When access to shared resources is declared using the `{ResourceLock}` annotation
or added with `{ResourceLocksProvider}` interface, the JUnit Jupiter engine uses
this information to ensure that no conflicting tests are run in parallel.
This guarantee extends to lifecycle methods of a test class or method.
For example, if a test method is annotated with a `{ResourceLock}` annotation,
the "lock" will be acquired before any `@BeforeEach` methods are executed
and released after all `@AfterEach` methods have been executed.
marcphilipp marked this conversation as resolved.
Show resolved Hide resolved

[NOTE]
.Running tests in isolation
Expand All @@ -2958,8 +2967,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}` interface
marcphilipp marked this conversation as resolved.
Show resolved Hide resolved
----
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,86 @@
/*
* 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.HashSet;
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 final class Provider implements ResourceLocksProvider {

@Override
public Set<Lock> provideForMethod(Class<?> testClass, Method testMethod) {
ResourceAccessMode mode;
if (testMethod.getName().equals("customPropertyIsNotSetByDefault")) {
marcphilipp marked this conversation as resolved.
Show resolved Hide resolved
mode = READ;
}
else {
mode = READ_WRITE;
}
Set<Lock> locks = new HashSet<>();
locks.add(new Lock(SYSTEM_PROPERTIES, mode));
return locks;
marcphilipp marked this conversation as resolved.
Show resolved Hide resolved
}
}

}
// 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
* in runtime via {@link ResourceLock#providers}.
marcphilipp marked this conversation as resolved.
Show resolved Hide resolved
*
* <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
Loading