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

Introduce adding 'shared resources' programmatically #3889

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7fc2b96
Introduce adding 'resource locks' programmatically.
VladimirDmitrienko Jul 31, 2024
cde4bc8
Add 'ResourceLocksProviderIntegrationTests'.
VladimirDmitrienko Aug 1, 2024
00788b6
Add JavaDoc.
VladimirDmitrienko Aug 6, 2024
ff3c786
Add 'locks from provider' cases to 'NodeTreeWalkerIntegrationTests'.
VladimirDmitrienko Aug 6, 2024
1c6eef8
Add 'LockTests'.
VladimirDmitrienko Aug 6, 2024
7d894c5
Add user guide.
VladimirDmitrienko Aug 7, 2024
1f8a6d0
Revert "Add 'locks from provider' cases to 'NodeTreeWalkerIntegration…
VladimirDmitrienko Aug 12, 2024
e1b5473
Move '@ResourceLocksFrom' functionality to '@ResourceLock'.
VladimirDmitrienko Aug 12, 2024
4460685
Add tests for '@ResourceLock' and 'ResourceLocksProvider'.
VladimirDmitrienko Aug 15, 2024
e14bfda
Merge branch 'main' of https://github.com/VladimirDmitrienko/junit5 i…
VladimirDmitrienko Aug 15, 2024
ba476e5
Fix formatting.
VladimirDmitrienko Aug 15, 2024
1d8488c
Polish Javadoc
marcphilipp Sep 18, 2024
3fddd55
Merge branch 'refs/heads/main' into #2677_add_exclusive_resource_prog…
VladimirDmitrienko Sep 20, 2024
49f9a28
Merge branch 'refs/heads/main' into #2677_add_exclusive_resource_prog…
VladimirDmitrienko Sep 25, 2024
d56196d
Remove version from User Guide.
VladimirDmitrienko Sep 25, 2024
c8d26ef
Revert formatting changes in User Guide.
VladimirDmitrienko Sep 25, 2024
9383c39
Mention that `ResourceLocksProvider` must have a no-args constructor.
VladimirDmitrienko Sep 27, 2024
249ab3e
Avoid searching for the annotation twice.
VladimirDmitrienko Sep 27, 2024
67e7dc5
Polish `DynamicSharedResourcesDemo`.
VladimirDmitrienko Sep 27, 2024
792b43a
Add Release Notes.
VladimirDmitrienko Sep 27, 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the User Guide is versioned, we usually avoid mentioning specific versions when features were introduced. Thus, please remove that bit.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. Done

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert the changes made to the formatting of these two paragraphs.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


[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
----
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")) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would make for a better example if we'd check for testMethod.getName().startsWith("canSet") and invert the if?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.
Done

mode = READ;
}
else {
mode = READ_WRITE;
}
Set<Lock> locks = new HashSet<>();
locks.add(new Lock(SYSTEM_PROPERTIES, mode));
return locks;
}
}

}
// 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,18 @@
* <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}.
*
* <p>Resources declared "statically" using {@code @ResourceLock(value, mode)}
* are combined with "dynamic" resources added via {@link ResourceLocksProvider.Lock}.
*
* @see Isolated
* @see Resources
* @see ResourceAccessMode
* @see ResourceLocks
* @see ResourceLocksProvider
* @see ResourceLocksProvider.Lock
* @since 5.3
*/
@API(status = STABLE, since = "5.10")
Expand All @@ -64,17 +73,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