Skip to content

Commit

Permalink
Add "TestSource" support to dynamic containers and tests
Browse files Browse the repository at this point in the history
This commit introduces the support to add an instance of `java.net.URI`
to a dynamic container or test by adding static factory methods that
take a test source uri as a parameter.

Prior to this commit the source for dynamic tests was always a
`MethodSource` pointing to the `@TestFactory` annotated method.
Now, for example, it is possible to refer to external files, that
are the underlying sources of generated containers and tests.

Addresses part of: junit-team#1178
  • Loading branch information
sormuras authored and Andrei94 committed Jun 23, 2018
1 parent b53ecd3 commit 7c632c5
Show file tree
Hide file tree
Showing 11 changed files with 326 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ on GitHub.
_alias_ for `Arguments.of()`. `arguments()` is intended to be used via `import static`.
* New `get<Class>(index)` Kotlin extension method to make `ArgumentsAccessor` friendlier
to use from Kotlin.
* New support for supplying a custom test source `URI` when creating a dynamic container
or test. See factory methods `dynamicContainer(String, URI, ...)` in `DynamicContainer`
and `dynamicTest(String, URI, Executable)` in `DynamicTest` for details.


[[release-notes-5.3.0-M1-junit-vintage]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import static org.apiguardian.api.API.Status.EXPERIMENTAL;

import java.net.URI;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

Expand Down Expand Up @@ -49,7 +50,7 @@ public class DynamicContainer extends DynamicNode {
* @see #dynamicContainer(String, Stream)
*/
public static DynamicContainer dynamicContainer(String displayName, Iterable<? extends DynamicNode> dynamicNodes) {
return new DynamicContainer(displayName, StreamSupport.stream(dynamicNodes.spliterator(), false));
return dynamicContainer(displayName, null, StreamSupport.stream(dynamicNodes.spliterator(), false));
}

/**
Expand All @@ -65,13 +66,32 @@ public static DynamicContainer dynamicContainer(String displayName, Iterable<? e
* @see #dynamicContainer(String, Iterable)
*/
public static DynamicContainer dynamicContainer(String displayName, Stream<? extends DynamicNode> dynamicNodes) {
return new DynamicContainer(displayName, dynamicNodes);
return dynamicContainer(displayName, null, dynamicNodes);
}

/**
* Factory for creating a new {@code DynamicContainer} for the supplied display
* name, the test source uri, and stream of dynamic nodes.
*
* <p>The stream of dynamic nodes must not contain {@code null} elements.
*
* @param displayName the display name for the dynamic container; never
* {@code null} or blank
* @param testSourceUri the test source uri for the dynamic test; can be {@code null}
* @param dynamicNodes stream of dynamic nodes to execute;
* never {@code null}
* @since 5.3
* @see #dynamicContainer(String, Iterable)
*/
public static DynamicContainer dynamicContainer(String displayName, URI testSourceUri,
Stream<? extends DynamicNode> dynamicNodes) {
return new DynamicContainer(displayName, testSourceUri, dynamicNodes);
}

private final Stream<? extends DynamicNode> children;

private DynamicContainer(String displayName, Stream<? extends DynamicNode> children) {
super(displayName);
private DynamicContainer(String displayName, URI testSourceUri, Stream<? extends DynamicNode> children) {
super(displayName, testSourceUri);
Preconditions.notNull(children, "children must not be null");
this.children = children;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@

import static org.apiguardian.api.API.Status.EXPERIMENTAL;

import java.net.URI;
import java.util.Optional;

import org.apiguardian.api.API;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.commons.util.ToStringBuilder;
Expand All @@ -29,8 +32,12 @@ public abstract class DynamicNode {

private final String displayName;

DynamicNode(String displayName) {
/** Custom test source {@link URI} instance associated with this node; potentially {@code null}. */
private final URI testSourceUri;

DynamicNode(String displayName, URI testSourceUri) {
this.displayName = Preconditions.notBlank(displayName, "displayName must not be null or blank");
this.testSourceUri = testSourceUri;
}

/**
Expand All @@ -40,9 +47,20 @@ public String getDisplayName() {
return this.displayName;
}

/**
* Get the optional test source of this {@code DynamicNode}.
* @since 5.3
*/
public Optional<URI> getTestSourceURI() {
return Optional.ofNullable(testSourceUri);
}

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

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import static java.util.Spliterators.spliteratorUnknownSize;
import static org.apiguardian.api.API.Status.EXPERIMENTAL;

import java.net.URI;
import java.util.Iterator;
import java.util.function.Function;
import java.util.stream.Stream;
Expand Down Expand Up @@ -59,7 +60,23 @@ public class DynamicTest extends DynamicNode {
* @see #stream(Iterator, Function, ThrowingConsumer)
*/
public static DynamicTest dynamicTest(String displayName, Executable executable) {
return new DynamicTest(displayName, executable);
return new DynamicTest(displayName, null, executable);
}

/**
* Factory for creating a new {@code DynamicTest} for the supplied display
* name, the test source uri, and executable code block.
*
* @param displayName the display name for the dynamic test; never
* {@code null} or blank
* @param testSourceUri the test source uri for the dynamic test; can be {@code null}
* @param executable the executable code block for the dynamic test;
* never {@code null}
* @since 5.3
* @see #stream(Iterator, Function, ThrowingConsumer)
*/
public static DynamicTest dynamicTest(String displayName, URI testSourceUri, Executable executable) {
return new DynamicTest(displayName, testSourceUri, executable);
}

/**
Expand Down Expand Up @@ -101,8 +118,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, URI testSourceUri, Executable executable) {
super(displayName, testSourceUri);
this.executable = Preconditions.notNull(executable, "executable must not be null");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.junit.platform.commons.util.PreconditionViolationException;
import org.junit.platform.engine.TestSource;
import org.junit.platform.engine.UniqueId;
import org.junit.platform.engine.support.descriptor.UriSource;

/**
* {@link org.junit.platform.engine.TestDescriptor TestDescriptor} for
Expand Down Expand Up @@ -108,10 +109,18 @@ private Stream<DynamicNode> toDynamicNodeStream(Object testFactoryMethodResult)
}
}

private JUnitException invalidReturnTypeException(Throwable cause) {
String message = String.format(
"@TestFactory method [%s] must return a Stream, Collection, Iterable, or Iterator of %s.",
getTestMethod().toGenericString(), DynamicNode.class.getName());
return new JUnitException(message, cause);
}

static Optional<JupiterTestDescriptor> createDynamicDescriptor(JupiterTestDescriptor parent, DynamicNode node,
int index, TestSource source, DynamicDescendantFilter dynamicDescendantFilter) {
int index, TestSource testSource, DynamicDescendantFilter dynamicDescendantFilter) {
UniqueId uniqueId;
Supplier<JupiterTestDescriptor> descriptorCreator;
TestSource source = computeOptionalTestSource(node).orElse(testSource);
if (node instanceof DynamicTest) {
DynamicTest test = (DynamicTest) node;
uniqueId = parent.getUniqueId().append(DYNAMIC_TEST_SEGMENT_TYPE, "#" + index);
Expand All @@ -131,11 +140,8 @@ static Optional<JupiterTestDescriptor> createDynamicDescriptor(JupiterTestDescri
return Optional.empty();
}

private JUnitException invalidReturnTypeException(Throwable cause) {
String message = String.format(
"@TestFactory method [%s] must return a Stream, Collection, Iterable, or Iterator of %s.",
getTestMethod().toGenericString(), DynamicNode.class.getName());
return new JUnitException(message, cause);
static Optional<TestSource> computeOptionalTestSource(DynamicNode node) {
return node.getTestSourceURI().map(UriSource::from);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,19 @@

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.junit.jupiter.api.function.Executable;
import org.junit.platform.commons.support.ReflectionSupport;
import org.opentest4j.AssertionFailedError;

Expand All @@ -30,6 +33,9 @@
*/
class DynamicTestTests {

private static final Executable nix = () -> {
};

private final List<String> assertedValues = new ArrayList<>();

@Test
Expand Down Expand Up @@ -79,6 +85,24 @@ void reflectiveOperationThrowingInvocationTargetException() {
assertThat(t50.getCause()).hasMessage("expected: <1> but was: <50>");
}

@Test
void testSourceUriIsNotPresentByDefault() {
DynamicTest test = dynamicTest("foo", nix);
assertThat(test.getTestSourceURI()).isNotPresent();
assertThat(dynamicContainer("bar", Stream.of(test)).getTestSourceURI()).isNotPresent();
}

@Test
void testSourceUriIsReturnedWhenSupplied() {
URI testSourceUri = URI.create("any://test");
DynamicTest test = dynamicTest("foo", testSourceUri, nix);
URI containerSourceUri = URI.create("other://container");
DynamicContainer container = dynamicContainer("bar", containerSourceUri, Stream.of(test));

assertThat(test.getTestSourceURI().get()).isSameAs(testSourceUri);
assertThat(container.getTestSourceURI().get()).isSameAs(containerSourceUri);
}

private void assert1Equals48Directly() {
Assertions.assertEquals(1, 48);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright 2015-2018 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
*
* http://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.platform.engine.support.descriptor;

import static org.apiguardian.api.API.Status.STABLE;

import java.net.URI;
import java.util.Objects;

import org.apiguardian.api.API;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.commons.util.ToStringBuilder;

/**
* Default uri-based test source implementation.
*
* @since 1.3
*/
@API(status = STABLE, since = "1.3")
class DefaultUriSource implements UriSource {

private static final long serialVersionUID = 1L;

private final URI uri;

DefaultUriSource(URI uri) {
this.uri = Preconditions.notNull(uri, "uri must not be null");
}

@Override
public URI getUri() {
return uri;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
DefaultUriSource that = (DefaultUriSource) o;
return Objects.equals(this.uri, that.uri);
}

@Override
public int hashCode() {
return Objects.hash(this.uri);
}

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

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
import java.util.Optional;

import org.apiguardian.api.API;
import org.junit.platform.commons.logging.LoggerFactory;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.commons.util.StringUtils;
import org.junit.platform.commons.util.ToStringBuilder;

/**
Expand Down Expand Up @@ -52,6 +54,50 @@ public static FilePosition from(int line, int column) {
return new FilePosition(line, column);
}

/**
* Create an optional {@code FilePosition} parsing the supplied
* {@code query} string.
*
* <p>Examples of valid {@code query} strings:
* <ul>
* <li>{@code "line=23"}</li>
* <li>{@code "line=23&column=42"}</li>
* </ul>
*
* @param query the query string; may be {@code null}
* @since 1.3
*/
public static Optional<FilePosition> fromQuery(String query) {
FilePosition result = null;
Integer line = null;
Integer column = null;
if (StringUtils.isNotBlank(query)) {
try {
for (String pair : query.split("&")) {
String[] data = pair.split("=");
if (data.length == 2) {
String key = data[0];
int value = Integer.valueOf(data[1]);
if (key.equals("line")) {
line = value;
}
if (key.equals("column")) {
column = value;
}
}
}
}
catch (IllegalArgumentException e) {
LoggerFactory.getLogger(FilePosition.class).debug(e, () -> "parsing query failed: " + query);
// fall-through and continue
}
if (line != null) {
result = column == null ? new FilePosition(line) : new FilePosition(line, column);
}
}
return Optional.ofNullable(result);
}

private final int line;
private final Integer column;

Expand All @@ -65,7 +111,7 @@ private FilePosition(int line, int column) {
Preconditions.condition(line > 0, "line number must be greater than zero");
Preconditions.condition(column > 0, "column number must be greater than zero");
this.line = line;
this.column = Integer.valueOf(column);
this.column = column;
}

/**
Expand Down
Loading

0 comments on commit 7c632c5

Please sign in to comment.