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 fail-fast feature for dynamic test execution #838

Closed
wants to merge 11 commits into from
43 changes: 43 additions & 0 deletions documentation/src/test/java/example/DynamicTestsDemo.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import java.util.stream.Stream;

import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.DynamicRuntime;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.TestFactory;
Expand All @@ -44,6 +45,48 @@ List<String> dynamicTestsWithInvalidReturnType() {
return Arrays.asList("Hello");
}

@TestFactory
DynamicNode[] dynamicNodesWithRequiredTests() {
// end::user_guide[]
// @formatter:off
// tag::user_guide[]
return new DynamicNode[] {
dynamicTest("Visit page requiring authorization while not logged in", () -> {
// attempt to visit page which requires that a user is logged in
// assert user is redirected to login page
}),
dynamicTest("Log-in", () -> {
// submit login form with valid credentials
// assert user is redirected back to previous page requiring authorization
// fail("you shall not pass");
},
// if login failed return "false" to break the dynamic execution loop here
DynamicRuntime::wasLastExecutableSuccessful
),
dynamicContainer("Can access several pages while logged in",
dynamicTest("Visit second page requiring authorization while logged in", () -> {
// visit another page which requires that a user is logged in
// assert user can access page
}),
dynamicTest("Visit third page requiring authorization while logged in", () -> {
// visit another page which requires that a user is logged in
// assert user can access page
}),
dynamicTest("Visit fourth page requiring authorization while logged in", () -> {
// visit another page which requires that a user is logged in
// assert user can access page
})
),
dynamicTest("Log-out", () -> {
// visit logout URL
// assert user has been logged out
})
};
// end::user_guide[]
// @formatter:on
// tag::user_guide[]
}

@TestFactory
Stream<DynamicNode> dynamicTestsWithContainers() {
// end::user_guide[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import static org.junit.platform.commons.meta.API.Usage.Experimental;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
Expand All @@ -28,6 +29,10 @@
@API(Experimental)
public class DynamicContainer extends DynamicNode {

public static DynamicContainer dynamicContainer(String displayName, DynamicNode... dynamicNodes) {
return new DynamicContainer(displayName, Arrays.stream(dynamicNodes));
}

public static DynamicContainer dynamicContainer(String displayName, Iterable<? extends DynamicNode> dynamicNodes) {
return new DynamicContainer(displayName, StreamSupport.stream(dynamicNodes.spliterator(), false));
}
Expand All @@ -39,13 +44,18 @@ public static DynamicContainer dynamicContainer(String displayName, Stream<? ext
private final List<DynamicNode> dynamicNodes;

private DynamicContainer(String displayName, Stream<? extends DynamicNode> dynamicNodes) {
super(displayName);
super(displayName, stayAlive -> true);
Preconditions.notNull(dynamicNodes, "dynamicNodes must not be null");
this.dynamicNodes = dynamicNodes.collect(CollectionUtils.toUnmodifiableList());
Preconditions.containsNoNullElements(this.dynamicNodes, "individual dynamic node must not be null");
Preconditions.notEmpty(this.dynamicNodes, "dynamic node collection passed to container must not be empty");
}

/**
* Get the dynamic child nodes.
*/
public Iterable<DynamicNode> getDynamicNodes() {
return dynamicNodes;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

import static org.junit.platform.commons.meta.API.Usage.Experimental;

import java.util.function.Predicate;

import org.junit.platform.commons.meta.API;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.commons.util.ToStringBuilder;
Expand All @@ -26,21 +28,27 @@
public abstract class DynamicNode {

private final String displayName;
private final Predicate<DynamicRuntime> stayAlive;

DynamicNode(String displayName) {
DynamicNode(String displayName, Predicate<DynamicRuntime> stayAlive) {
this.displayName = Preconditions.notBlank(displayName, "displayName must not be null or blank");
this.stayAlive = Preconditions.notNull(stayAlive, "stayAlive predicate must not be null");
}

/**
* Get the display name of this {@code DynamicTest}.
* Get the display name of this {@code DynamicNode}.
*/
public String getDisplayName() {
return this.displayName;
}

public boolean breaking(DynamicRuntime runtimeInformation) {
return !stayAlive.test(runtimeInformation);
}

@Override
public String toString() {
return new ToStringBuilder(this).append("displayName", displayName).toString();
return new ToStringBuilder(this).append("displayName", displayName).append("stayAlive", stayAlive).toString();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2015-2017 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 v1.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/

package org.junit.jupiter.api;

import static org.junit.platform.commons.meta.API.Usage.Experimental;

import java.time.Instant;

import org.junit.platform.commons.meta.API;

/**
* Dynamic runtime information.
*
* @since 5.0
*/
@API(Experimental)
public interface DynamicRuntime {

Instant getInstantOfTestFactoryStart();

boolean wasLastExecutableSuccessful();
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import java.util.Iterator;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

Expand Down Expand Up @@ -51,14 +52,35 @@ public class DynamicTest extends DynamicNode {
* Factory for creating a new {@code DynamicTest} for the supplied display
* name and executable code block.
*
* <p>An unsuccessful execution result does not break the execution loop.
*
* @param displayName the display name for the dynamic test; never
* {@code null} or blank
* @param executable the executable code block for the dynamic test;
* never {@code null}
* @see #dynamicTest(String, Executable, Predicate)
* @see #stream(Iterator, Function, ThrowingConsumer)
*/
public static DynamicTest dynamicTest(String displayName, Executable executable) {
return new DynamicTest(displayName, executable);
return new DynamicTest(displayName, executable, __ -> true);
}

/**
* Factory for creating a new required {@code DynamicTest} for the supplied
* display name and executable code block.
*
* <p>It is up to the user-supplied predicate whether the execution loop breaks.
*
* @param displayName the display name for the dynamic test; never
* {@code null} or blank
* @param executable the executable code block for the dynamic test;
* never {@code null}
* @see #dynamicTest(String, Executable)
* @see #stream(Iterator, Function, ThrowingConsumer)
*/
public static DynamicTest dynamicTest(String displayName, Executable executable,
Predicate<DynamicRuntime> stayAlive) {
return new DynamicTest(displayName, executable, stayAlive);
}

/**
Expand Down Expand Up @@ -100,8 +122,8 @@ public static <T> Stream<DynamicTest> stream(Iterator<T> inputGenerator,

private final Executable executable;

private DynamicTest(String displayName, Executable executable) {
super(displayName);
private DynamicTest(String displayName, Executable executable, Predicate<DynamicRuntime> stayAlive) {
super(displayName, stayAlive);
this.executable = Preconditions.notNull(executable, "executable must not be null");
}

Expand All @@ -111,4 +133,5 @@ private DynamicTest(String displayName, Executable executable) {
public Executable getExecutable() {
return this.executable;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@

package org.junit.jupiter.engine.descriptor;

import static org.junit.jupiter.engine.descriptor.TestFactoryTestDescriptor.createDynamicDescriptor;
import java.util.Optional;

import org.junit.jupiter.api.DynamicContainer;
import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.engine.TestSource;
import org.junit.platform.engine.UniqueId;

Expand All @@ -26,11 +27,14 @@
*/
class DynamicContainerTestDescriptor extends JupiterTestDescriptor {

private final TestFactoryTestDescriptor root;
private final DynamicContainer dynamicContainer;
private final TestSource testSource;

DynamicContainerTestDescriptor(UniqueId uniqueId, DynamicContainer dynamicContainer, TestSource testSource) {
DynamicContainerTestDescriptor(TestFactoryTestDescriptor root, UniqueId uniqueId, DynamicContainer dynamicContainer,
TestSource testSource) {
super(uniqueId, dynamicContainer.getDisplayName());
this.root = root;
this.dynamicContainer = dynamicContainer;
this.testSource = testSource;
setSource(testSource);
Expand All @@ -46,7 +50,11 @@ public JupiterEngineExecutionContext execute(JupiterEngineExecutionContext conte
DynamicTestExecutor dynamicTestExecutor) throws Exception {
int index = 1;
for (DynamicNode childNode : dynamicContainer.getDynamicNodes()) {
dynamicTestExecutor.execute(createDynamicDescriptor(this, childNode, index++, testSource));
JupiterTestDescriptor childDescriptor = root.createDynamicDescriptor(this, childNode, index++, testSource);
Optional<TestExecutionResult> optionalResult = dynamicTestExecutor.execute(childDescriptor);
if (root.breaking(childNode, optionalResult)) {
break;
}
}
return context;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@
import static org.junit.platform.commons.meta.API.Usage.Internal;

import java.lang.reflect.Method;
import java.time.Instant;
import java.util.Iterator;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;

import org.junit.jupiter.api.DynamicContainer;
import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.DynamicRuntime;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.extension.TestExtensionContext;
import org.junit.jupiter.engine.execution.ExecutableInvoker;
Expand All @@ -26,12 +30,14 @@
import org.junit.platform.commons.meta.API;
import org.junit.platform.commons.util.CollectionUtils;
import org.junit.platform.commons.util.PreconditionViolationException;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.engine.TestSource;
import org.junit.platform.engine.UniqueId;

/**
* {@link org.junit.platform.engine.TestDescriptor TestDescriptor} for {@link org.junit.jupiter.api.TestFactory @TestFactory}
* methods.
* {@link org.junit.platform.engine.TestDescriptor TestDescriptor} for
* {@link org.junit.jupiter.api.TestFactory @TestFactory} methods.
*
* @since 5.0
*/
Expand All @@ -43,8 +49,13 @@ public class TestFactoryTestDescriptor extends MethodTestDescriptor {

private static final ExecutableInvoker executableInvoker = new ExecutableInvoker();

private final AtomicBoolean broken;
private final Instant start;

public TestFactoryTestDescriptor(UniqueId uniqueId, Class<?> testClass, Method testMethod) {
super(uniqueId, testClass, testMethod);
this.broken = new AtomicBoolean(false);
this.start = Instant.now();
}

// --- TestDescriptor ------------------------------------------------------
Expand Down Expand Up @@ -75,8 +86,14 @@ protected void invokeTestMethod(JupiterEngineExecutionContext context, DynamicTe
Iterator<DynamicNode> iterator = dynamicNodeStream.iterator();
while (iterator.hasNext()) {
DynamicNode dynamicNode = iterator.next();
int currentIndex = index;
Preconditions.notNull(dynamicNode, () -> "dynamic node #" + currentIndex
+ " must not be null. [testMethod=" + getTestMethod() + "]");
JupiterTestDescriptor descriptor = createDynamicDescriptor(this, dynamicNode, index++, source);
dynamicTestExecutor.execute(descriptor);
Optional<TestExecutionResult> optionalResult = dynamicTestExecutor.execute(descriptor);
if (breaking(dynamicNode, optionalResult)) {
break;
}
}
}
catch (ClassCastException ex) {
Expand All @@ -95,7 +112,32 @@ private Stream<DynamicNode> toDynamicNodeStream(Object testFactoryMethodResult)
}
}

static JupiterTestDescriptor createDynamicDescriptor(JupiterTestDescriptor parent, DynamicNode node, int index,
boolean breaking(DynamicNode dynamicNode, Optional<TestExecutionResult> testExecutionResult) {
// already broken? stay broken.
if (broken.get()) {
return true;
}
// alive, let node decide what to do...
class Info implements DynamicRuntime {
@Override
public Instant getInstantOfTestFactoryStart() {
return start;
}

@Override
public boolean wasLastExecutableSuccessful() {
return testExecutionResult.map(TestExecutionResult::isSuccessful).orElse(false);
}
}
if (dynamicNode.breaking(new Info())) {
broken.set(true);
return true;
}
// alive, and still here? stay alive.
return false;
}

JupiterTestDescriptor createDynamicDescriptor(JupiterTestDescriptor parent, DynamicNode node, int index,
TestSource source) {
JupiterTestDescriptor descriptor;
if (node instanceof DynamicTest) {
Expand All @@ -106,7 +148,7 @@ static JupiterTestDescriptor createDynamicDescriptor(JupiterTestDescriptor paren
else {
DynamicContainer container = (DynamicContainer) node;
UniqueId uniqueId = parent.getUniqueId().append(DYNAMIC_CONTAINER_SEGMENT_TYPE, "#" + index);
descriptor = new DynamicContainerTestDescriptor(uniqueId, container, source);
descriptor = new DynamicContainerTestDescriptor(this, uniqueId, container, source);
}
parent.addChild(descriptor);
return descriptor;
Expand Down
Loading