diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicContainer.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicContainer.java index 2bdf44fda25d..fbc379ad92c6 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicContainer.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicContainer.java @@ -13,11 +13,18 @@ import static org.apiguardian.api.API.Status.MAINTAINED; import java.net.URI; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; import java.util.stream.Stream; import java.util.stream.StreamSupport; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.parallel.ExecutionMode; import org.junit.platform.commons.util.Preconditions; /** @@ -38,6 +45,8 @@ @API(status = MAINTAINED, since = "5.3") public class DynamicContainer extends DynamicNode { + private final @Nullable ExecutionMode defaultChildExecutionMode; + /** * Factory for creating a new {@code DynamicContainer} for the supplied display * name and collection of dynamic nodes. @@ -51,7 +60,7 @@ public class DynamicContainer extends DynamicNode { * @see #dynamicContainer(String, Stream) */ public static DynamicContainer dynamicContainer(String displayName, Iterable dynamicNodes) { - return dynamicContainer(displayName, null, StreamSupport.stream(dynamicNodes.spliterator(), false)); + return dynamicContainer(config -> config.displayName(displayName).children(dynamicNodes)); } /** @@ -67,7 +76,7 @@ public static DynamicContainer dynamicContainer(String displayName, Iterable dynamicNodes) { - return dynamicContainer(displayName, null, dynamicNodes); + return dynamicContainer(config -> config.displayName(displayName).children(dynamicNodes)); } /** @@ -88,15 +97,21 @@ public static DynamicContainer dynamicContainer(String displayName, Stream dynamicNodes) { - return new DynamicContainer(displayName, testSourceUri, dynamicNodes); + return dynamicContainer(config -> config.displayName(displayName).source(testSourceUri).children(dynamicNodes)); + } + + public static DynamicContainer dynamicContainer(Consumer configurer) { + var configuration = new DefaultConfiguration(); + configurer.accept(configuration); + return new DynamicContainer(configuration); } private final Stream children; - private DynamicContainer(String displayName, @Nullable URI testSourceUri, Stream children) { - super(displayName, testSourceUri); - Preconditions.notNull(children, "children must not be null"); - this.children = children; + private DynamicContainer(DefaultConfiguration configuration) { + super(configuration); + this.children = Preconditions.notNull(configuration.children, "children must not be null"); + this.defaultChildExecutionMode = configuration.defaultChildExecutionMode; } /** @@ -107,4 +122,101 @@ public Stream getChildren() { return children; } + public Optional getDefaultChildExecutionMode() { + return Optional.ofNullable(defaultChildExecutionMode); + } + + public interface Configuration extends DynamicNode.Configuration { + + @Override + Configuration displayName(String displayName); + + @Override + Configuration source(@Nullable URI testSourceUri); + + @Override + Configuration executionCondition( + Function condition); + + @Override + Configuration executionMode(ExecutionMode executionMode); + + @Override + Configuration executionMode(ExecutionMode executionMode, String reason); + + Configuration defaultChildExecutionMode(ExecutionMode executionMode); + + Configuration defaultChildExecutionMode(ExecutionMode executionMode, String reason); + + default Configuration children(Iterable children) { + Preconditions.notNull(children, "children must not be null"); + return children(StreamSupport.stream(children.spliterator(), false)); + } + + default Configuration children(DynamicNode... children) { + Preconditions.notNull(children, "children must not be null"); + Preconditions.containsNoNullElements(children, "children must not contain null elements"); + return children(List.of(children)); + } + + Configuration children(Stream children); + + } + + private static class DefaultConfiguration extends AbstractConfiguration implements Configuration { + + private @Nullable Stream children; + private @Nullable ExecutionMode defaultChildExecutionMode; + + @Override + public Configuration displayName(String displayName) { + super.displayName(displayName); + return this; + } + + @Override + public Configuration source(@Nullable URI testSourceUri) { + super.source(testSourceUri); + return this; + } + + @Override + public Configuration executionCondition( + Function condition) { + super.executionCondition(condition); + return this; + } + + @Override + public Configuration executionMode(ExecutionMode executionMode) { + super.executionMode(executionMode); + return this; + } + + @Override + public Configuration executionMode(ExecutionMode executionMode, String reason) { + super.executionMode(executionMode, reason); + return this; + } + + @Override + public Configuration defaultChildExecutionMode(ExecutionMode executionMode) { + this.defaultChildExecutionMode = executionMode; + return this; + } + + @Override + public Configuration defaultChildExecutionMode(ExecutionMode executionMode, String reason) { + defaultChildExecutionMode(executionMode); + return this; + } + + @Override + public Configuration children(Stream children) { + Preconditions.notNull(children, "children must not be null"); + Preconditions.condition(this.children == null, "children can only be set once"); + this.children = children; + return this; + } + } } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicNode.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicNode.java index 59c990ee8b99..9281257d2f53 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicNode.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicNode.java @@ -14,9 +14,13 @@ import java.net.URI; import java.util.Optional; +import java.util.function.Function; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.parallel.ExecutionMode; import org.junit.platform.commons.util.Preconditions; import org.junit.platform.commons.util.ToStringBuilder; @@ -34,12 +38,16 @@ public abstract class DynamicNode { private final String displayName; /** Custom test source {@link URI} associated with this node; potentially {@code null}. */ - @Nullable - private final URI testSourceUri; + private final @Nullable URI testSourceUri; - DynamicNode(String displayName, @Nullable URI testSourceUri) { - this.displayName = Preconditions.notBlank(displayName, "displayName must not be null or blank"); - this.testSourceUri = testSourceUri; + private final @Nullable ExecutionMode executionMode; + private final @Nullable Function executionCondition; + + DynamicNode(AbstractConfiguration configuration) { + this.displayName = Preconditions.notBlank(configuration.displayName, "displayName must not be null or blank"); + this.testSourceUri = configuration.testSourceUri; + this.executionMode = configuration.executionMode; + this.executionCondition = configuration.executionCondition; } /** @@ -62,6 +70,14 @@ public Optional getTestSourceUri() { return Optional.ofNullable(testSourceUri); } + public Optional getExecutionMode() { + return Optional.ofNullable(executionMode); + } + + public Optional> getExecutionCondition() { + return Optional.ofNullable(executionCondition); + } + @Override public String toString() { return new ToStringBuilder(this) // @@ -70,4 +86,58 @@ public String toString() { .toString(); } + public interface Configuration { + + Configuration displayName(String displayName); + + Configuration source(@Nullable URI testSourceUri); + + Configuration executionCondition( + Function condition); + + Configuration executionMode(ExecutionMode executionMode); + + Configuration executionMode(ExecutionMode executionMode, String reason); + + } + + abstract static class AbstractConfiguration implements Configuration { + + private @Nullable String displayName; + private @Nullable URI testSourceUri; + private @Nullable ExecutionMode executionMode; + private @Nullable Function executionCondition; + + @Override + public Configuration displayName(String displayName) { + this.displayName = displayName; + return this; + } + + @Override + public Configuration source(@Nullable URI testSourceUri) { + this.testSourceUri = testSourceUri; + return this; + } + + @Override + public Configuration executionCondition( + Function condition) { + // TODO Handle multiple calls + this.executionCondition = condition; + return this; + } + + @Override + public Configuration executionMode(ExecutionMode executionMode) { + this.executionMode = executionMode; + return this; + } + + @Override + public Configuration executionMode(ExecutionMode executionMode, String reason) { + return executionMode(executionMode); + } + } + } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicTest.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicTest.java index 82e8018cb0ba..4d88968b2a8d 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicTest.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicTest.java @@ -17,14 +17,18 @@ import java.net.URI; import java.util.Iterator; +import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Stream; import java.util.stream.StreamSupport; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.function.Executable; import org.junit.jupiter.api.function.ThrowingConsumer; +import org.junit.jupiter.api.parallel.ExecutionMode; import org.junit.platform.commons.util.Preconditions; /** @@ -62,7 +66,7 @@ public class DynamicTest extends DynamicNode { * @see #stream(Iterator, Function, ThrowingConsumer) */ public static DynamicTest dynamicTest(String displayName, Executable executable) { - return new DynamicTest(displayName, null, executable); + return dynamicTest(config -> config.displayName(displayName).executable(executable)); } /** @@ -80,7 +84,13 @@ public static DynamicTest dynamicTest(String displayName, Executable executable) * @see #stream(Iterator, Function, ThrowingConsumer) */ public static DynamicTest dynamicTest(String displayName, @Nullable URI testSourceUri, Executable executable) { - return new DynamicTest(displayName, testSourceUri, executable); + return dynamicTest(config -> config.displayName(displayName).source(testSourceUri).executable(executable)); + } + + public static DynamicTest dynamicTest(Consumer configurer) { + var configuration = new DefaultConfiguration(); + configurer.accept(configuration); + return new DynamicTest(configuration); } /** @@ -291,9 +301,9 @@ public static , E extends Executable> Stream str private final Executable executable; - private DynamicTest(String displayName, @Nullable URI testSourceUri, Executable executable) { - super(displayName, testSourceUri); - this.executable = Preconditions.notNull(executable, "executable must not be null"); + private DynamicTest(DefaultConfiguration configuration) { + super(configuration); + this.executable = Preconditions.notNull(configuration.executable, "executable must not be null"); } /** @@ -303,4 +313,67 @@ public Executable getExecutable() { return this.executable; } + public interface Configuration extends DynamicNode.Configuration { + + @Override + Configuration displayName(String displayName); + + @Override + Configuration source(@Nullable URI testSourceUri); + + @Override + Configuration executionCondition( + Function condition); + + @Override + Configuration executionMode(ExecutionMode executionMode); + + @Override + Configuration executionMode(ExecutionMode executionMode, String reason); + + Configuration executable(Executable executable); + } + + private static class DefaultConfiguration extends AbstractConfiguration implements Configuration { + + private @Nullable Executable executable; + + @Override + public Configuration displayName(String displayName) { + super.displayName(displayName); + return this; + } + + @Override + public Configuration source(@Nullable URI testSourceUri) { + super.source(testSourceUri); + return this; + } + + @Override + public Configuration executionCondition( + Function condition) { + super.executionCondition(condition); + return this; + } + + @Override + public Configuration executionMode(ExecutionMode executionMode) { + super.executionMode(executionMode); + return this; + } + + @Override + public Configuration executionMode(ExecutionMode executionMode, String reason) { + super.executionMode(executionMode, reason); + return this; + } + + @Override + public Configuration executable(Executable executable) { + this.executable = executable; + return this; + } + } + } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ConditionEvaluationResult.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ConditionEvaluationResult.java index 5b683e43f32b..bbb5bc68b03e 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ConditionEvaluationResult.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ConditionEvaluationResult.java @@ -15,6 +15,7 @@ import java.util.Optional; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.junit.platform.commons.util.StringUtils; import org.junit.platform.commons.util.ToStringBuilder; @@ -32,7 +33,7 @@ public class ConditionEvaluationResult { * @param reason the reason why the container or test should be enabled * @return an enabled {@code ConditionEvaluationResult} with the given reason */ - public static ConditionEvaluationResult enabled(String reason) { + public static ConditionEvaluationResult enabled(@Nullable String reason) { return new ConditionEvaluationResult(true, reason); } @@ -42,7 +43,7 @@ public static ConditionEvaluationResult enabled(String reason) { * @param reason the reason why the container or test should be disabled * @return a disabled {@code ConditionEvaluationResult} with the given reason */ - public static ConditionEvaluationResult disabled(String reason) { + public static ConditionEvaluationResult disabled(@Nullable String reason) { return new ConditionEvaluationResult(false, reason); } @@ -67,7 +68,7 @@ public static ConditionEvaluationResult disabled(String reason, String customRea private final Optional reason; - private ConditionEvaluationResult(boolean enabled, String reason) { + private ConditionEvaluationResult(boolean enabled, @Nullable String reason) { this.enabled = enabled; this.reason = Optional.ofNullable(reason); } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicContainerTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicContainerTestDescriptor.java index c5ade66011e3..ec69e8f0045d 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicContainerTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicContainerTestDescriptor.java @@ -58,6 +58,11 @@ public Type getType() { return Type.CONTAINER; } + @Override + Optional getDefaultChildExecutionMode() { + return this.dynamicContainer.getDefaultChildExecutionMode().map(JupiterTestDescriptor::toExecutionMode); + } + @Override public JupiterEngineExecutionContext execute(JupiterEngineExecutionContext context, DynamicTestExecutor dynamicTestExecutor) throws Exception { diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicNodeTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicNodeTestDescriptor.java index f68c98b480bf..b2c9e1b5c0c2 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicNodeTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicNodeTestDescriptor.java @@ -10,8 +10,12 @@ package org.junit.jupiter.engine.descriptor; +import java.util.Optional; +import java.util.function.Function; + import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.extension.ConditionEvaluationResult; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; @@ -27,11 +31,20 @@ abstract class DynamicNodeTestDescriptor extends JupiterTestDescriptor { protected final int index; + private final Optional executionMode; + private final Optional> executionCondition; DynamicNodeTestDescriptor(UniqueId uniqueId, int index, DynamicNode dynamicNode, @Nullable TestSource testSource, JupiterConfiguration configuration) { super(uniqueId, dynamicNode.getDisplayName(), testSource, configuration); this.index = index; + this.executionMode = dynamicNode.getExecutionMode().map(JupiterTestDescriptor::toExecutionMode); + this.executionCondition = dynamicNode.getExecutionCondition(); + } + + @Override + Optional getExplicitExecutionMode() { + return executionMode; } @Override @@ -58,7 +71,10 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte @Override public SkipResult shouldBeSkipped(JupiterEngineExecutionContext context) { - return SkipResult.doNotSkip(); + return this.executionCondition // + .map(condition -> condition.apply(context.getExtensionContext())) // + .map(this::toSkipResult) // + .orElse(SkipResult.doNotSkip()); } } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java index 8bd1c7941b5a..a8f971bc561d 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java @@ -191,7 +191,7 @@ public SkipResult shouldBeSkipped(JupiterEngineExecutionContext context) { return toSkipResult(evaluationResult); } - private SkipResult toSkipResult(ConditionEvaluationResult evaluationResult) { + protected SkipResult toSkipResult(ConditionEvaluationResult evaluationResult) { if (evaluationResult.isDisabled()) { return SkipResult.skip(evaluationResult.getReason().orElse("")); }