Skip to content

Commit 8a9d199

Browse files
Hans Zuidervaarthanszt
authored andcommitted
Extension of to stream converter method.
Examples of benefits: - Kotlin Sequence support for @testfactory - Kotlin Sequence support for @MethodSource - Classes that expose an Iterator returning method, can be converted to a stream. Issue: #3376 I hereby agree to the terms of the JUnit Contributor License Agreement.
1 parent d013085 commit 8a9d199

File tree

6 files changed

+186
-9
lines changed

6 files changed

+186
-9
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2416,7 +2416,7 @@ generated at runtime by a factory method that is annotated with `@TestFactory`.
24162416
In contrast to `@Test` methods, a `@TestFactory` method is not itself a test case but
24172417
rather a factory for test cases. Thus, a dynamic test is the product of a factory.
24182418
Technically speaking, a `@TestFactory` method must return a single `DynamicNode` or a
2419-
`Stream`, `Collection`, `Iterable`, `Iterator`, or array of `DynamicNode` instances.
2419+
`Stream`, `Collection`, `Iterable`, `Iterator`, an `Iterator` providing class or array of `DynamicNode` instances.
24202420
Instantiable subclasses of `DynamicNode` are `DynamicContainer` and `DynamicTest`.
24212421
`DynamicContainer` instances are composed of a _display name_ and a list of dynamic child
24222422
nodes, enabling the creation of arbitrarily nested hierarchies of dynamic nodes.

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ private Stream<DynamicNode> toDynamicNodeStream(Object testFactoryMethodResult)
131131

132132
private JUnitException invalidReturnTypeException(Throwable cause) {
133133
String message = String.format(
134-
"@TestFactory method [%s] must return a single %2$s or a Stream, Collection, Iterable, Iterator, or array of %2$s.",
134+
"@TestFactory method [%s] must return a single %2$s or a Stream, Collection, Iterable, Iterator, Iterator-source or array of %2$s.",
135135
getTestMethod().toGenericString(), DynamicNode.class.getName());
136136
return new JUnitException(message, cause);
137137
}

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

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static org.apiguardian.api.API.Status.INTERNAL;
1919

2020
import java.lang.reflect.Array;
21+
import java.lang.reflect.Method;
2122
import java.util.Arrays;
2223
import java.util.Collection;
2324
import java.util.Collections;
@@ -27,6 +28,7 @@
2728
import java.util.ListIterator;
2829
import java.util.Optional;
2930
import java.util.Set;
31+
import java.util.Spliterator;
3032
import java.util.function.Consumer;
3133
import java.util.stream.Collector;
3234
import java.util.stream.DoubleStream;
@@ -35,7 +37,9 @@
3537
import java.util.stream.Stream;
3638

3739
import org.apiguardian.api.API;
40+
import org.junit.platform.commons.JUnitException;
3841
import org.junit.platform.commons.PreconditionViolationException;
42+
import org.junit.platform.commons.function.Try;
3943

4044
/**
4145
* Collection of utilities for working with {@link Collection Collections}.
@@ -122,7 +126,7 @@ public static <T> Set<T> toSet(T[] values) {
122126
* returned, so if more control over the returned list is required,
123127
* consider creating a new {@code Collector} implementation like the
124128
* following:
125-
*
129+
* <p>
126130
* <pre class="code">
127131
* public static &lt;T&gt; Collector&lt;T, ?, List&lt;T&gt;&gt; toUnmodifiableList(Supplier&lt;List&lt;T&gt;&gt; listSupplier) {
128132
* return Collectors.collectingAndThen(Collectors.toCollection(listSupplier), Collections::unmodifiableList);
@@ -162,7 +166,11 @@ public static boolean isConvertibleToStream(Class<?> type) {
162166
|| Iterable.class.isAssignableFrom(type)//
163167
|| Iterator.class.isAssignableFrom(type)//
164168
|| Object[].class.isAssignableFrom(type)//
165-
|| (type.isArray() && type.getComponentType().isPrimitive()));
169+
|| (type.isArray() && type.getComponentType().isPrimitive())//
170+
|| Arrays.stream(type.getMethods())//
171+
.filter(m -> m.getName().equals("iterator"))//
172+
.map(Method::getReturnType)//
173+
.anyMatch(returnType -> returnType == Iterator.class));
166174
}
167175

168176
/**
@@ -178,6 +186,7 @@ public static boolean isConvertibleToStream(Class<?> type) {
178186
* <li>{@link Iterator}</li>
179187
* <li>{@link Object} array</li>
180188
* <li>primitive array</li>
189+
* <li>An object that contains a method with name `iterator` returning an Iterator object</li>
181190
* </ul>
182191
*
183192
* @param object the object to convert into a stream; never {@code null}
@@ -224,8 +233,31 @@ public static Stream<?> toStream(Object object) {
224233
if (object.getClass().isArray() && object.getClass().getComponentType().isPrimitive()) {
225234
return IntStream.range(0, Array.getLength(object)).mapToObj(i -> Array.get(object, i));
226235
}
227-
throw new PreconditionViolationException(
228-
"Cannot convert instance of " + object.getClass().getName() + " into a Stream: " + object);
236+
return tryConvertToStreamByReflection(object);
237+
}
238+
239+
private static Stream<?> tryConvertToStreamByReflection(Object object) {
240+
Preconditions.notNull(object, "Object must not be null");
241+
try {
242+
String name = "iterator";
243+
Method method = object.getClass().getMethod(name);
244+
if (method.getReturnType() == Iterator.class) {
245+
return stream(() -> tryIteratorToSpliterator(object, method), ORDERED, false);
246+
}
247+
else {
248+
throw new PreconditionViolationException(
249+
"Method with name 'iterator' does not return " + Iterator.class.getName());
250+
}
251+
}
252+
catch (NoSuchMethodException | IllegalStateException e) {
253+
throw new PreconditionViolationException(//
254+
"Cannot convert instance of " + object.getClass().getName() + " into a Stream: " + object, e);
255+
}
256+
}
257+
258+
private static Spliterator<?> tryIteratorToSpliterator(Object object, Method method) {
259+
return Try.call(() -> spliteratorUnknownSize((Iterator<?>) method.invoke(object), ORDERED))//
260+
.getOrThrow(e -> new JUnitException("Cannot invoke method " + method.getName() + " onto " + object, e));//
229261
}
230262

231263
/**
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2015-2024 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: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2015-2024 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.aggregator
11+
12+
import org.junit.jupiter.api.Assertions.assertEquals
13+
import org.junit.jupiter.params.ParameterizedTest
14+
import org.junit.jupiter.params.provider.Arguments.arguments
15+
import org.junit.jupiter.params.provider.MethodSource
16+
import java.time.Month
17+
18+
/**
19+
* Tests for ParameterizedTest kotlin compatibility
20+
*/
21+
object KotlinParameterizedTests {
22+
@ParameterizedTest
23+
@MethodSource("dataProvidedByKotlinSequence")
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 dataProvidedByKotlinSequence() =
33+
sequenceOf(
34+
arguments(1, Month.JANUARY),
35+
arguments(3, Month.MARCH),
36+
arguments(8, Month.AUGUST),
37+
arguments(5, Month.MAY),
38+
arguments(12, Month.DECEMBER)
39+
)
40+
}

platform-tests/src/test/java/org/junit/platform/commons/util/CollectionUtilsTests.java

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import java.util.Iterator;
2727
import java.util.List;
2828
import java.util.Set;
29+
import java.util.Spliterator;
30+
import java.util.Spliterators;
2931
import java.util.concurrent.atomic.AtomicBoolean;
3032
import java.util.stream.DoubleStream;
3133
import java.util.stream.IntStream;
@@ -140,6 +142,7 @@ class StreamConversion {
140142
Collection.class, //
141143
Iterable.class, //
142144
Iterator.class, //
145+
IteratorProvider.class, //
143146
Object[].class, //
144147
String[].class, //
145148
int[].class, //
@@ -161,10 +164,11 @@ static Stream<Object> objectsConvertibleToStreams() {
161164
Stream.of("cat", "dog"), //
162165
DoubleStream.of(42.3), //
163166
IntStream.of(99), //
164-
LongStream.of(100000000), //
167+
LongStream.of(100_000_000), //
165168
Set.of(1, 2, 3), //
166169
Arguments.of((Object) new Object[] { 9, 8, 7 }), //
167-
new int[] { 5, 10, 15 }//
170+
new int[] { 5, 10, 15 }, //
171+
IteratorProvider.of(new Integer[] { 1, 2, 3, 4, 5 })//
168172
);
169173
}
170174

@@ -175,6 +179,8 @@ static Stream<Object> objectsConvertibleToStreams() {
175179
Object.class, //
176180
Integer.class, //
177181
String.class, //
182+
IteratorProviderNotUsable.class, //
183+
Spliterator.class, //
178184
int.class, //
179185
boolean.class //
180186
})
@@ -243,7 +249,7 @@ void toStreamWithLongStream() {
243249
}
244250

245251
@Test
246-
@SuppressWarnings({ "unchecked", "serial" })
252+
@SuppressWarnings({ "unchecked" })
247253
void toStreamWithCollection() {
248254
var collectionStreamClosed = new AtomicBoolean(false);
249255
Collection<String> input = new ArrayList<>() {
@@ -288,6 +294,24 @@ void toStreamWithIterator() {
288294
assertThat(result).containsExactly("foo", "bar");
289295
}
290296

297+
@Test
298+
@SuppressWarnings("unchecked")
299+
void toStreamWithIteratorProvider() {
300+
final var input = IteratorProvider.of(new String[] { "foo", "bar" });
301+
302+
final var result = (Stream<String>) CollectionUtils.toStream(input);
303+
304+
assertThat(result).containsExactly("foo", "bar");
305+
}
306+
307+
@Test
308+
void throwWhenIteratorNamedMethodDoesNotReturnAnIterator() {
309+
var o = IteratorProviderNotUsable.of(new String[] { "Test" });
310+
var e = assertThrows(PreconditionViolationException.class, () -> CollectionUtils.toStream(o));
311+
312+
assertEquals("Method with name 'iterator' does not return java.util.Iterator", e.getMessage());
313+
}
314+
291315
@Test
292316
@SuppressWarnings("unchecked")
293317
void toStreamWithArray() {
@@ -356,4 +380,29 @@ public Object convert(Object source, ParameterContext context) throws ArgumentCo
356380
}
357381
}
358382
}
383+
384+
/**
385+
* An interface that has a method with name 'iterator', returning a java.util/Iterator as a return type
386+
*/
387+
private interface IteratorProvider<T> {
388+
389+
@SuppressWarnings("unused")
390+
Iterator<T> iterator();
391+
392+
static <T> IteratorProvider<T> of(T[] elements) {
393+
return () -> Spliterators.iterator(Arrays.spliterator(elements));
394+
}
395+
}
396+
397+
/**
398+
* An interface that has a method with name 'iterator', but does not return java.util/Iterator as a return type
399+
*/
400+
private interface IteratorProviderNotUsable {
401+
@SuppressWarnings("unused")
402+
Object iterator();
403+
404+
static <T> IteratorProviderNotUsable of(T[] elements) {
405+
return () -> Spliterators.iterator(Arrays.spliterator(elements));
406+
}
407+
}
359408
}

0 commit comments

Comments
 (0)