Skip to content

Commit 4083551

Browse files
hansztmarcphilipp
andauthored
Add support for converting Kotlin Sequence to Stream (#3377)
Kotlin's `Sequence` type may now be used as return type of `@TestFactory` and `@MethodSource`-referenced methods as well as fields referenced by `@FieldSource` annotations. Resolves #3376. --------- Co-authored-by: Marc Philipp <[email protected]>
1 parent dc06d09 commit 4083551

File tree

11 files changed

+221
-39
lines changed

11 files changed

+221
-39
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-RC1.adoc

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

48-
* ❓
48+
* Add support for Kotlin `Sequence` to `@MethodSource`, `@FieldSource`, and
49+
`@TestFactory`.
4950

5051

5152
[[release-notes-5.13.0-RC1-junit-vintage]]

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

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1949,10 +1949,11 @@ of the annotated `@ParameterizedClass` or `@ParameterizedTest`. Generally speaki
19491949
translates to a `Stream` of `Arguments` (i.e., `Stream<Arguments>`); however, the actual
19501950
concrete return type can take on many forms. In this context, a "stream" is anything that
19511951
JUnit can reliably convert into a `Stream`, such as `Stream`, `DoubleStream`,
1952-
`LongStream`, `IntStream`, `Collection`, `Iterator`, `Iterable`, an array of objects, or
1953-
an array of primitives. The "arguments" within the stream can be supplied as an instance
1954-
of `Arguments`, an array of objects (e.g., `Object[]`), or a single value if the
1955-
parameterized class or test method accepts a single argument.
1952+
`LongStream`, `IntStream`, `Collection`, `Iterator`, `Iterable`, an array of objects or
1953+
primitives, or any type that provides an `iterator(): Iterator` method (such as, for
1954+
example, a `kotlin.sequences.Sequence`). The "arguments" within the stream can be supplied
1955+
as an instance of `Arguments`, an array of objects (e.g., `Object[]`), or a single value
1956+
if the parameterized class or test method accepts a single argument.
19561957

19571958
If the return type is `Stream` or one of the primitive streams,
19581959
JUnit will properly close it by calling `BaseStream.close()`,
@@ -2038,10 +2039,11 @@ In this context, a "stream" is anything that JUnit can reliably convert to a `St
20382039
however, the actual concrete field type can take on many forms. Generally speaking this
20392040
translates to a `Collection`, an `Iterable`, a `Supplier` of a stream (`Stream`,
20402041
`DoubleStream`, `LongStream`, or `IntStream`), a `Supplier` of an `Iterator`, an array of
2041-
objects, or an array of primitives. Each set of "arguments" within the "stream" can be
2042-
supplied as an instance of `Arguments`, an array of objects (for example, `Object[]`,
2043-
`String[]`, etc.), or a single value if the parameterized class or test method accepts a
2044-
single argument.
2042+
objects or primitives, or any type that provides an `iterator(): Iterator` method (such
2043+
as, for example, a `kotlin.sequences.Sequence`). Each set of "arguments" within the
2044+
"stream" can be supplied as an instance of `Arguments`, an array of objects (for example,
2045+
`Object[]`, `String[]`, etc.), or a single value if the parameterized class ortest method accepts
2046+
a single argument.
20452047

20462048
[WARNING]
20472049
====
@@ -2870,7 +2872,11 @@ generated at runtime by a factory method that is annotated with `@TestFactory`.
28702872
In contrast to `@Test` methods, a `@TestFactory` method is not itself a test case but
28712873
rather a factory for test cases. Thus, a dynamic test is the product of a factory.
28722874
Technically speaking, a `@TestFactory` method must return a single `DynamicNode` or a
2873-
`Stream`, `Collection`, `Iterable`, `Iterator`, or array of `DynamicNode` instances.
2875+
_stream_ of `DynamicNode` instances or any of its subclasses. In this context, a "stream"
2876+
is anything that JUnit can reliably convert into a `Stream`, such as `Stream`,
2877+
`Collection`, `Iterator`, `Iterable`, an array of objects, or any type that provides an
2878+
`iterator(): Iterator` method (such as, for example, a `kotlin.sequences.Sequence`).
2879+
28742880
Instantiable subclasses of `DynamicNode` are `DynamicContainer` and `DynamicTest`.
28752881
`DynamicContainer` instances are composed of a _display name_ and a list of dynamic child
28762882
nodes, enabling the creation of arbitrarily nested hierarchies of dynamic nodes.

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
*
3131
* <p>{@code @TestFactory} methods must not be {@code private} or {@code static}
3232
* and must return a {@code Stream}, {@code Collection}, {@code Iterable},
33-
* {@code Iterator}, or array of {@link DynamicNode} instances. Supported
33+
* {@code Iterator}, array of {@link DynamicNode} instances, or any type that
34+
* provides an {@link java.util.Iterator Iterator}-returning {@code iterator()}
35+
* method (such as, for example, a {@code kotlin.sequences.Sequence}). Supported
3436
* subclasses of {@code DynamicNode} include {@link DynamicContainer} and
3537
* {@link DynamicTest}. <em>Dynamic tests</em> will be executed lazily,
3638
* enabling dynamic and even non-deterministic generation of test cases.

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestFactoryMethod.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
public class IsTestFactoryMethod extends IsTestableMethod {
4141

4242
private static final String EXPECTED_RETURN_TYPE_MESSAGE = String.format(
43-
"must return a single %1$s or a Stream, Collection, Iterable, Iterator, or array of %1$s",
43+
"must return a single %1$s or a Stream, Collection, Iterable, Iterator, Iterator provider, or array of %1$s",
4444
DynamicNode.class.getName());
4545

4646
public IsTestFactoryMethod(DiscoveryIssueReporter issueReporter) {

junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSource.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,13 @@
4444
* {@link java.util.stream.DoubleStream DoubleStream},
4545
* {@link java.util.stream.LongStream LongStream}, or
4646
* {@link java.util.stream.IntStream IntStream}), a {@code Supplier} of an
47-
* {@link java.util.Iterator Iterator}, an array of objects, or an array of
48-
* primitives. Each set of "arguments" within the "stream" can be supplied as an
49-
* instance of {@link Arguments}, an array of objects (for example, {@code Object[]},
50-
* {@code String[]}, etc.), or a single <em>value</em> if the parameterized
51-
* class or test accepts a single argument.
47+
* {@link java.util.Iterator Iterator}, an array of objects or primitives, or
48+
* any type that provides an {@link java.util.Iterator Iterator}-returning
49+
* {@code iterator()} method (such as, for example, a
50+
* {@code kotlin.sequences.Sequence}). Each set of "arguments" within the
51+
* "stream" can be supplied as an instance of {@link Arguments}, an array of
52+
* objects (for example, {@code Object[]}, {@code String[]}, etc.), or a single
53+
* <em>value</em> if the parameterized class or test accepts a single argument.
5254
*
5355
* <p>In contrast to the supported return types for {@link MethodSource @MethodSource}
5456
* factory methods, the value of a {@code @FieldSource} field cannot be an instance of

junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSource.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,13 @@
4343
* {@link java.util.stream.LongStream LongStream},
4444
* {@link java.util.stream.IntStream IntStream},
4545
* {@link java.util.Collection Collection},
46-
* {@link java.util.Iterator Iterator},
47-
* {@link Iterable}, an array of objects, or an array of primitives. Each set of
48-
* "arguments" within the "stream" can be supplied as an instance of
49-
* {@link Arguments}, an array of objects (e.g., {@code Object[]},
50-
* {@code String[]}, etc.), or a single <em>value</em> if the parameterized test
51-
* method accepts a single argument.
46+
* {@link java.util.Iterator Iterator}, an array of objects or primitives, or
47+
* any type that provides an {@link java.util.Iterator Iterator}-returning
48+
* {@code iterator()} method (such as, for example, a
49+
* {@code kotlin.sequences.Sequence}). Each set of "arguments" within the
50+
* "stream" can be supplied as an instance of {@link Arguments}, an array of
51+
* objects (e.g., {@code Object[]}, {@code String[]}, etc.), or a single
52+
* <em>value</em> if the parameterized test method accepts a single argument.
5253
*
5354
* <p>If the return type is {@code Stream} or
5455
* one of the primitive streams, JUnit will properly close it by calling

junit-platform-commons/src/main/java/org/junit/platform/commons/util/CollectionUtils.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
import static java.util.stream.Collectors.toList;
1717
import static java.util.stream.StreamSupport.stream;
1818
import static org.apiguardian.api.API.Status.INTERNAL;
19+
import static org.junit.platform.commons.support.ReflectionSupport.invokeMethod;
1920

2021
import java.lang.reflect.Array;
22+
import java.lang.reflect.Method;
2123
import java.util.Arrays;
2224
import java.util.Collection;
2325
import java.util.Collections;
@@ -36,6 +38,7 @@
3638

3739
import org.apiguardian.api.API;
3840
import org.junit.platform.commons.PreconditionViolationException;
41+
import org.junit.platform.commons.support.ReflectionSupport;
3942

4043
/**
4144
* Collection of utilities for working with {@link Collection Collections}.
@@ -122,7 +125,7 @@ public static <T> Set<T> toSet(T[] values) {
122125
* returned, so if more control over the returned list is required,
123126
* consider creating a new {@code Collector} implementation like the
124127
* following:
125-
*
128+
* <p>
126129
* <pre class="code">
127130
* public static &lt;T&gt; Collector&lt;T, ?, List&lt;T&gt;&gt; toUnmodifiableList(Supplier&lt;List&lt;T&gt;&gt; listSupplier) {
128131
* return Collectors.collectingAndThen(Collectors.toCollection(listSupplier), Collections::unmodifiableList);
@@ -161,7 +164,8 @@ public static boolean isConvertibleToStream(Class<?> type) {
161164
|| Iterable.class.isAssignableFrom(type)//
162165
|| Iterator.class.isAssignableFrom(type)//
163166
|| Object[].class.isAssignableFrom(type)//
164-
|| (type.isArray() && type.getComponentType().isPrimitive()));
167+
|| (type.isArray() && type.getComponentType().isPrimitive())//
168+
|| findIteratorMethod(type).isPresent());
165169
}
166170

167171
/**
@@ -177,6 +181,9 @@ public static boolean isConvertibleToStream(Class<?> type) {
177181
* <li>{@link Iterator}</li>
178182
* <li>{@link Object} array</li>
179183
* <li>primitive array</li>
184+
* <li>any type that provides an
185+
* {@link java.util.Iterator Iterator}-returning {@code iterator()} method
186+
* (such as, for example, a {@code kotlin.sequences.Sequence})</li>
180187
* </ul>
181188
*
182189
* @param object the object to convert into a stream; never {@code null}
@@ -223,8 +230,21 @@ public static Stream<?> toStream(Object object) {
223230
if (object.getClass().isArray() && object.getClass().getComponentType().isPrimitive()) {
224231
return IntStream.range(0, Array.getLength(object)).mapToObj(i -> Array.get(object, i));
225232
}
226-
throw new PreconditionViolationException(
227-
"Cannot convert instance of " + object.getClass().getName() + " into a Stream: " + object);
233+
return tryConvertToStreamByReflection(object);
234+
}
235+
236+
private static Stream<?> tryConvertToStreamByReflection(Object object) {
237+
return findIteratorMethod(object.getClass()) //
238+
.map(method -> (Iterator<?>) invokeMethod(method, object)) //
239+
.map(iterator -> spliteratorUnknownSize(iterator, ORDERED)) //
240+
.map(spliterator -> stream(spliterator, false)) //
241+
.orElseThrow(() -> new PreconditionViolationException(String.format(
242+
"Cannot convert instance of %s into a Stream: %s", object.getClass().getName(), object)));
243+
}
244+
245+
private static Optional<Method> findIteratorMethod(Class<?> type) {
246+
return ReflectionSupport.findMethod(type, "iterator") //
247+
.filter(method -> method.getReturnType() == Iterator.class);
228248
}
229249

230250
/**

jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsTestFactoryMethodTests.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ void invalidFactoryMethods(String methodName) {
6666
var issue = getOnlyElement(discoveryIssues);
6767
assertThat(issue.severity()).isEqualTo(DiscoveryIssue.Severity.WARNING);
6868
assertThat(issue.message()).isEqualTo(
69-
"@TestFactory method '%s' must return a single org.junit.jupiter.api.DynamicNode or a Stream, Collection, Iterable, Iterator, or array of org.junit.jupiter.api.DynamicNode. "
69+
"@TestFactory method '%s' must return a single org.junit.jupiter.api.DynamicNode or a "
70+
+ "Stream, Collection, Iterable, Iterator, Iterator provider, or array of org.junit.jupiter.api.DynamicNode. "
7071
+ "It will not be executed.",
7172
method.toGenericString());
7273
assertThat(issue.source()).contains(MethodSource.from(method));
@@ -83,7 +84,8 @@ void suspiciousFactoryMethods(String methodName) {
8384
assertThat(issue.severity()).isEqualTo(DiscoveryIssue.Severity.INFO);
8485
assertThat(issue.message()).isEqualTo(
8586
"The declared return type of @TestFactory method '%s' does not support static validation. "
86-
+ "It must return a single org.junit.jupiter.api.DynamicNode or a Stream, Collection, Iterable, Iterator, or array of org.junit.jupiter.api.DynamicNode.",
87+
+ "It must return a single org.junit.jupiter.api.DynamicNode or a "
88+
+ "Stream, Collection, Iterable, Iterator, Iterator provider, or array of org.junit.jupiter.api.DynamicNode.",
8789
method.toGenericString());
8890
assertThat(issue.source()).contains(MethodSource.from(method));
8991
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2015-2025 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
package org.junit.jupiter.api
11+
12+
import org.junit.jupiter.api.Assertions.assertEquals
13+
import org.junit.jupiter.api.DynamicTest.dynamicTest
14+
import java.math.BigDecimal
15+
import java.math.BigDecimal.ONE
16+
import java.math.MathContext
17+
import java.math.BigInteger as BigInt
18+
import java.math.RoundingMode as Rounding
19+
20+
/**
21+
* Unit tests for JUnit Jupiter [TestFactory] use in kotlin classes.
22+
*
23+
* @since 5.12
24+
*/
25+
class KotlinDynamicTests {
26+
@Nested
27+
inner class SequenceReturningTestFactoryTests {
28+
@TestFactory
29+
fun `Dynamic tests returned as Kotlin sequence`() =
30+
generateSequence(0) { it + 2 }
31+
.map { dynamicTest("$it should be even") { assertEquals(0, it % 2) } }
32+
.take(10)
33+
34+
@TestFactory
35+
fun `Consecutive fibonacci nr ratios, should converge to golden ratio as n increases`(): Sequence<DynamicTest> {
36+
val scale = 5
37+
val goldenRatio =
38+
(ONE + 5.toBigDecimal().sqrt(MathContext(scale + 10, Rounding.HALF_UP)))
39+
.divide(2.toBigDecimal(), scale, Rounding.HALF_UP)
40+
41+
fun shouldApproximateGoldenRatio(
42+
cur: BigDecimal,
43+
next: BigDecimal
44+
) = next.divide(cur, scale, Rounding.HALF_UP).let {
45+
dynamicTest("$cur / $next = $it should approximate the golden ratio in $scale decimals") {
46+
assertEquals(goldenRatio, it)
47+
}
48+
}
49+
return generateSequence(BigInt.ONE to BigInt.ONE) { (cur, next) -> next to cur + next }
50+
.map { (cur) -> cur.toBigDecimal() }
51+
.zipWithNext(::shouldApproximateGoldenRatio)
52+
.drop(14)
53+
.take(10)
54+
}
55+
}
56+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2015-2025 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
package org.junit.jupiter.params
11+
12+
import org.junit.jupiter.api.Assertions.assertEquals
13+
import org.junit.jupiter.params.provider.Arguments.arguments
14+
import org.junit.jupiter.params.provider.FieldSource
15+
import org.junit.jupiter.params.provider.MethodSource
16+
import java.time.Month
17+
18+
/**
19+
* Tests for Kotlin compatibility of ParameterizedTest
20+
*/
21+
object ParameterizedTestKotlinSequenceIntegrationTests {
22+
@ParameterizedTest
23+
@MethodSource("dataProvidedByKotlinSequenceMethod")
24+
fun `a method source can be supplied by a Sequence-returning method`(
25+
value: Int,
26+
month: Month
27+
) {
28+
assertEquals(value, month.value)
29+
}
30+
31+
@JvmStatic
32+
private fun dataProvidedByKotlinSequenceMethod() = dataProvidedByKotlinSequenceField
33+
34+
@JvmStatic
35+
val dataProvidedByKotlinSequenceField =
36+
sequenceOf(
37+
arguments(1, Month.JANUARY),
38+
arguments(3, Month.MARCH),
39+
arguments(8, Month.AUGUST),
40+
arguments(5, Month.MAY),
41+
arguments(12, Month.DECEMBER)
42+
)
43+
44+
@ParameterizedTest
45+
@FieldSource("dataProvidedByKotlinSequenceField")
46+
fun `a field source can be supplied by a Sequence-typed field`(
47+
value: Int,
48+
month: Month
49+
) {
50+
assertEquals(value, month.value)
51+
}
52+
}

0 commit comments

Comments
 (0)