Skip to content

Commit 1f7be03

Browse files
authored
Allow configuring execution mode of dynamic tests and containers (#4621)
* Introduce new `dynamicTest(Consumer<? super Configuration>)` factory method for dynamic tests. It allows configuring the `ExecutionMode` of the dynamic test in addition to its display name, test source URI, and executable. * Introduce new `dynamicContainer(Consumer<? super Configuration>)` factory method for dynamic containers. It allows configuring the `ExecutionMode` of the dynamic container and/or its children in addition to its display name, test source URI, and children. Resolves #2497.
1 parent cee1471 commit 1f7be03

File tree

14 files changed

+667
-40
lines changed

14 files changed

+667
-40
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-6.1.0-M1.adoc

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,12 @@ repository on GitHub.
4545
[[release-notes-6.1.0-M1-junit-jupiter-new-features-and-improvements]]
4646
==== New Features and Improvements
4747

48-
* ❓
48+
* Introduce new `dynamicTest(Consumer<? super Configuration>)` factory method for dynamic
49+
tests. It allows configuring the `ExecutionMode` of the dynamic test in addition to its
50+
display name, test source URI, and executable.
51+
* Introduce new `dynamicContainer(Consumer<? super Configuration>)` factory method for
52+
dynamic containers. It allows configuring the `ExecutionMode` of the dynamic container
53+
and/or its children in addition to its display name, test source URI, and children.
4954

5055

5156
[[release-notes-6.1.0-M1-junit-vintage]]

documentation/src/docs/asciidoc/user-guide/writing-tests.adoc

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3106,6 +3106,29 @@ implementations.
31063106
`UriSource` ::
31073107
If none of the above `TestSource` implementations are applicable.
31083108

3109+
[[writing-tests-dynamic-tests-parallel-execution]]
3110+
==== Parallel Execution
3111+
3112+
Dynamic tests and containers support
3113+
<<writing-tests-parallel-execution, parallel execution>>. You can configure their
3114+
`ExecutionMode` by using the `dynamicTest(Consumer)` and `dynamicContainer(Consumer)`
3115+
factory methods as illustrated by the following example.
3116+
3117+
[source,java,indent=0]
3118+
----
3119+
include::{testDir}/example/DynamicTestsDemo.java[tags=execution_mode]
3120+
----
3121+
3122+
Executing the above test factory method results in the following test tree and execution
3123+
modes:
3124+
3125+
* dynamicTestsWithConfiguredExecutionMode() -- `CONCURRENT` (from `@Execution` annotation)
3126+
** Container A -- `CONCURRENT` (from `@Execution` annotation)
3127+
*** not null -- `SAME_THREAD` (from `executionMode(...)` call)
3128+
*** properties -- `CONCURRENT` (from `@Execution` annotation)
3129+
**** length > 0 -- `CONCURRENT` (from `executionMode(...)` call)
3130+
**** not empty -- `SAME_THREAD` (from `childExecutionMode(...)` call)
3131+
** ... (same for "Container B" and "Container C")
31093132

31103133
[[writing-tests-declarative-timeouts]]
31113134
=== Timeouts

documentation/src/test/java/example/DynamicTestsDemo.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import static org.junit.jupiter.api.Assertions.assertTrue;
1919
import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
2020
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
21+
import static org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT;
22+
import static org.junit.jupiter.api.parallel.ExecutionMode.SAME_THREAD;
2123

2224
import java.util.Arrays;
2325
import java.util.Collection;
@@ -35,6 +37,7 @@
3537
import org.junit.jupiter.api.Tag;
3638
import org.junit.jupiter.api.TestFactory;
3739
import org.junit.jupiter.api.function.ThrowingConsumer;
40+
import org.junit.jupiter.api.parallel.Execution;
3841

3942
// end::user_guide[]
4043
// @formatter:off
@@ -163,6 +166,44 @@ Stream<DynamicNode> dynamicTestsWithContainers() {
163166
)));
164167
}
165168

169+
// end::user_guide[]
170+
// tag::execution_mode[]
171+
@TestFactory
172+
@Execution(CONCURRENT) // <1>
173+
Stream<DynamicNode> dynamicTestsWithConfiguredExecutionMode() {
174+
return Stream.of("A", "B", "C")
175+
.map(input ->
176+
dynamicContainer(outer -> outer
177+
.displayName("Container " + input)
178+
.children(
179+
dynamicTest(config -> config
180+
.displayName("not null")
181+
.executionMode(SAME_THREAD) // <2>
182+
.executable(() -> assertNotNull(input))
183+
),
184+
dynamicContainer(inner -> inner
185+
.displayName("properties")
186+
.executionMode(CONCURRENT) // <3>
187+
.childExecutionMode(SAME_THREAD) // <4>
188+
.children(
189+
dynamicTest(config -> config
190+
.displayName("length > 0")
191+
.executionMode(CONCURRENT) // <5>
192+
.executable(() -> assertTrue(input.length() > 0))
193+
),
194+
dynamicTest(config -> config
195+
.displayName("not empty")
196+
.executable(() -> assertFalse(input.isEmpty()))
197+
)
198+
)
199+
)
200+
)
201+
)
202+
);
203+
}
204+
// end::execution_mode[]
205+
206+
// tag::user_guide[]
166207
@TestFactory
167208
DynamicNode dynamicNodeSingleTest() {
168209
return dynamicTest("'pop' is a palindrome", () -> assertTrue(isPalindrome("pop")));

junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicContainer.java

Lines changed: 132 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,19 @@
1010

1111
package org.junit.jupiter.api;
1212

13+
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
1314
import static org.apiguardian.api.API.Status.MAINTAINED;
1415

1516
import java.net.URI;
17+
import java.util.List;
18+
import java.util.Optional;
19+
import java.util.function.Consumer;
1620
import java.util.stream.Stream;
1721
import java.util.stream.StreamSupport;
1822

1923
import org.apiguardian.api.API;
2024
import org.jspecify.annotations.Nullable;
25+
import org.junit.jupiter.api.parallel.ExecutionMode;
2126
import org.junit.platform.commons.util.Preconditions;
2227

2328
/**
@@ -38,6 +43,8 @@
3843
@API(status = MAINTAINED, since = "5.3")
3944
public class DynamicContainer extends DynamicNode {
4045

46+
private final @Nullable ExecutionMode childExecutionMode;
47+
4148
/**
4249
* Factory for creating a new {@code DynamicContainer} for the supplied display
4350
* name and collection of dynamic nodes.
@@ -51,7 +58,7 @@ public class DynamicContainer extends DynamicNode {
5158
* @see #dynamicContainer(String, Stream)
5259
*/
5360
public static DynamicContainer dynamicContainer(String displayName, Iterable<? extends DynamicNode> dynamicNodes) {
54-
return dynamicContainer(displayName, null, StreamSupport.stream(dynamicNodes.spliterator(), false));
61+
return dynamicContainer(config -> config.displayName(displayName).children(dynamicNodes));
5562
}
5663

5764
/**
@@ -67,7 +74,7 @@ public static DynamicContainer dynamicContainer(String displayName, Iterable<? e
6774
* @see #dynamicContainer(String, Iterable)
6875
*/
6976
public static DynamicContainer dynamicContainer(String displayName, Stream<? extends DynamicNode> dynamicNodes) {
70-
return dynamicContainer(displayName, null, dynamicNodes);
77+
return dynamicContainer(config -> config.displayName(displayName).children(dynamicNodes));
7178
}
7279

7380
/**
@@ -88,15 +95,32 @@ public static DynamicContainer dynamicContainer(String displayName, Stream<? ext
8895
public static DynamicContainer dynamicContainer(String displayName, @Nullable URI testSourceUri,
8996
Stream<? extends DynamicNode> dynamicNodes) {
9097

91-
return new DynamicContainer(displayName, testSourceUri, dynamicNodes);
98+
return dynamicContainer(
99+
config -> config.displayName(displayName).testSourceUri(testSourceUri).children(dynamicNodes));
100+
}
101+
102+
/**
103+
* Factory for creating a new {@code DynamicTest} that is configured via the
104+
* supplied {@link Consumer} of {@link DynamicTest.Configuration}.
105+
*
106+
* @param configurer callback for configuring the resulting
107+
* {@code DynamicTest}; never {@code null}.
108+
*
109+
* @since 6.1
110+
*/
111+
@API(status = EXPERIMENTAL, since = "6.1")
112+
public static DynamicContainer dynamicContainer(Consumer<? super Configuration> configurer) {
113+
var configuration = new DefaultConfiguration();
114+
configurer.accept(configuration);
115+
return new DynamicContainer(configuration);
92116
}
93117

94118
private final Stream<? extends DynamicNode> children;
95119

96-
private DynamicContainer(String displayName, @Nullable URI testSourceUri, Stream<? extends DynamicNode> children) {
97-
super(displayName, testSourceUri);
98-
Preconditions.notNull(children, "children must not be null");
99-
this.children = children;
120+
private DynamicContainer(DefaultConfiguration configuration) {
121+
super(configuration);
122+
this.children = Preconditions.notNull(configuration.children, "children must not be null");
123+
this.childExecutionMode = configuration.childExecutionMode;
100124
}
101125

102126
/**
@@ -107,4 +131,105 @@ public Stream<? extends DynamicNode> getChildren() {
107131
return children;
108132
}
109133

134+
/**
135+
* {@return the {@link ExecutionMode} for
136+
* {@linkplain #getChildren() children} of this {@code DynamicContainer}
137+
* that is used unless they are
138+
* {@linkplain DynamicTest#getExecutionMode() configured} differently}.
139+
*
140+
* @since 6.1
141+
* @see DynamicTest#getExecutionMode()
142+
*/
143+
@API(status = EXPERIMENTAL, since = "6.1")
144+
public Optional<ExecutionMode> getChildExecutionMode() {
145+
return Optional.ofNullable(childExecutionMode);
146+
}
147+
148+
/**
149+
* {@code Configuration} of a {@link DynamicContainer}.
150+
*
151+
* @since 6.1
152+
* @see DynamicContainer#dynamicContainer(Consumer)
153+
*/
154+
@API(status = EXPERIMENTAL, since = "6.1")
155+
public sealed interface Configuration extends DynamicNode.Configuration<Configuration> {
156+
157+
/**
158+
* Set the
159+
* {@linkplain DynamicContainer#getChildExecutionMode() child execution mode}
160+
* to use for the configured {@link DynamicContainer}.
161+
*
162+
* @return this configuration for method chaining
163+
*/
164+
Configuration childExecutionMode(ExecutionMode executionMode);
165+
166+
/**
167+
* Set the {@linkplain DynamicContainer#getChildren() children} of the
168+
* configured {@link DynamicContainer}.
169+
*
170+
* <p>Any previously configured value is overridden.
171+
*
172+
* @param children the children; never {@code null} or containing
173+
* {@code null} elements
174+
* @return this configuration for method chaining
175+
*/
176+
default Configuration children(Iterable<? extends DynamicNode> children) {
177+
Preconditions.notNull(children, "children must not be null");
178+
return children(StreamSupport.stream(children.spliterator(), false));
179+
}
180+
181+
/**
182+
* Set the {@linkplain DynamicContainer#getChildren() children} of the
183+
* configured {@link DynamicContainer}.
184+
*
185+
* <p>Any previously configured value is overridden.
186+
*
187+
* @param children the children; never {@code null} or containing
188+
* {@code null} elements
189+
* @return this configuration for method chaining
190+
*/
191+
default Configuration children(DynamicNode... children) {
192+
Preconditions.notNull(children, "children must not be null");
193+
Preconditions.containsNoNullElements(children, "children must not contain null elements");
194+
return children(List.of(children));
195+
}
196+
197+
/**
198+
* Set the {@linkplain DynamicContainer#getChildren() children} of the
199+
* configured {@link DynamicContainer}.
200+
*
201+
* <p>Any previously configured value is overridden.
202+
*
203+
* @param children the children; never {@code null} or containing
204+
* {@code null} elements
205+
* @return this configuration for method chaining
206+
*/
207+
Configuration children(Stream<? extends DynamicNode> children);
208+
209+
}
210+
211+
static final class DefaultConfiguration extends AbstractConfiguration<Configuration> implements Configuration {
212+
213+
private @Nullable Stream<? extends DynamicNode> children;
214+
private @Nullable ExecutionMode childExecutionMode;
215+
216+
@Override
217+
public Configuration childExecutionMode(ExecutionMode executionMode) {
218+
this.childExecutionMode = Preconditions.notNull(executionMode, "executionMode must not be null");
219+
return this;
220+
}
221+
222+
@Override
223+
public Configuration children(Stream<? extends DynamicNode> children) {
224+
Preconditions.notNull(children, "children must not be null");
225+
Preconditions.condition(this.children == null, "children can only be set once");
226+
this.children = children;
227+
return this;
228+
}
229+
230+
@Override
231+
protected Configuration self() {
232+
return this;
233+
}
234+
}
110235
}

0 commit comments

Comments
 (0)