Skip to content

Commit 8a0a767

Browse files
author
Hans Zuidervaart
committed
Kotlin Sequence support for @testfactory
Classes that expose an Iterator returning method, can be converted to a stream. Classes that expose a Spliterator returning method, can be converted to a stream. ```markdown --- I hereby agree to the terms of the JUnit Contributor License Agreement. ```
1 parent eb34ed7 commit 8a0a767

File tree

5 files changed

+384
-8
lines changed

5 files changed

+384
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright 2015-2023 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.Assertions.assertFalse
14+
import org.junit.jupiter.api.Assertions.assertTrue
15+
import org.junit.jupiter.api.DynamicTest.dynamicTest
16+
import java.math.BigDecimal
17+
import java.math.BigDecimal.ONE
18+
import java.math.MathContext
19+
import java.math.BigInteger as BigInt
20+
import java.math.RoundingMode as Rounding
21+
22+
/**
23+
* Unit tests for JUnit Jupiter [TestFactory] use in kotlin classes.
24+
*
25+
* @since 5.10
26+
*/
27+
class KotlinDynamicTests {
28+
29+
@Nested
30+
inner class SequenceReturningTestFactoryTests {
31+
32+
@TestFactory
33+
fun `Dynamic tests returned as Kotlin sequence`() = generateSequence(0) { it + 2 }
34+
.map { dynamicTest("$it should be even") { assertTrue(it % 2 == 0) } }
35+
.take(10)
36+
37+
@TestFactory
38+
fun `Is anagram tests`(): Sequence<DynamicTest> {
39+
infix fun CharSequence.isAngramOf(other: CharSequence) = groupBy { it } == other.groupBy { it }
40+
41+
infix fun CharSequence.`should be an anagram of`(other: CharSequence) =
42+
dynamicTest("'$this' should be an anagram of '$other'") { assertTrue(this isAngramOf other) }
43+
44+
infix fun CharSequence.`should not be an anagram of`(other: CharSequence) =
45+
dynamicTest("'$this' should not be an anagram of '$other'") { assertFalse(this isAngramOf other) }
46+
47+
return sequenceOf(
48+
"a gentleman" `should be an anagram of` "elegant man",
49+
"laptop machines" `should be an anagram of` "apple macintosh",
50+
"salvador dali" `should be an anagram of` "avida dollars",
51+
"a gentleman" `should not be an anagram of` "spider man",
52+
"laptop computers" `should not be an anagram of` "apple macintosh",
53+
"salvador dali" `should not be an anagram of` "picasso"
54+
)
55+
}
56+
57+
@TestFactory
58+
fun `Consecutive fibonacci nr ratios, should converge to golden ratio as n increases`(): Sequence<DynamicTest> {
59+
val scale = 5
60+
val goldenRatio = (ONE + 5.toBigDecimal().sqrt(MathContext(scale + 10, Rounding.HALF_UP)))
61+
.divide(2.toBigDecimal(), scale, Rounding.HALF_UP)
62+
63+
fun shouldApproximateGoldenRatio(cur: BigDecimal, next: BigDecimal) =
64+
next.divide(cur, scale, Rounding.HALF_UP).let {
65+
dynamicTest("$cur / $next = $it should approximate the golden ratio in $scale decimals") {
66+
assertEquals(goldenRatio, it)
67+
}
68+
}
69+
return generateSequence(BigInt.ONE to BigInt.ONE) { (cur, next) -> next to cur + next }
70+
.map { (cur) -> cur.toBigDecimal() }
71+
.zipWithNext(::shouldApproximateGoldenRatio)
72+
.drop(14)
73+
.take(10)
74+
}
75+
}
76+
}

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

+14-4
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;
@@ -26,6 +27,7 @@
2627
import java.util.List;
2728
import java.util.ListIterator;
2829
import java.util.Set;
30+
import java.util.Spliterator;
2931
import java.util.function.Consumer;
3032
import java.util.stream.Collector;
3133
import java.util.stream.DoubleStream;
@@ -99,7 +101,7 @@ public static <T> Set<T> toSet(T[] values) {
99101
* returned, so if more control over the returned list is required,
100102
* consider creating a new {@code Collector} implementation like the
101103
* following:
102-
*
104+
* <p>
103105
* <pre class="code">
104106
* public static &lt;T&gt; Collector&lt;T, ?, List&lt;T&gt;&gt; toUnmodifiableList(Supplier&lt;List&lt;T&gt;&gt; listSupplier) {
105107
* return Collectors.collectingAndThen(Collectors.toCollection(listSupplier), Collections::unmodifiableList);
@@ -138,8 +140,12 @@ public static boolean isConvertibleToStream(Class<?> type) {
138140
|| LongStream.class.isAssignableFrom(type)//
139141
|| Iterable.class.isAssignableFrom(type)//
140142
|| Iterator.class.isAssignableFrom(type)//
143+
|| Spliterator.class.isAssignableFrom(type)//
141144
|| Object[].class.isAssignableFrom(type)//
142-
|| (type.isArray() && type.getComponentType().isPrimitive()));
145+
|| (type.isArray() && type.getComponentType().isPrimitive())//
146+
|| Arrays.stream(type.getMethods())//
147+
.map(Method::getReturnType)//
148+
.anyMatch(returnType -> returnType == Iterator.class || returnType == Spliterator.class));
143149
}
144150

145151
/**
@@ -153,8 +159,10 @@ public static boolean isConvertibleToStream(Class<?> type) {
153159
* <li>{@link Collection}</li>
154160
* <li>{@link Iterable}</li>
155161
* <li>{@link Iterator}</li>
162+
* <li>{@link Spliterator}</li>
156163
* <li>{@link Object} array</li>
157164
* <li>primitive array</li>
165+
* <li>An object that contains an iterator or spliterator returning method</li>
158166
* </ul>
159167
*
160168
* @param object the object to convert into a stream; never {@code null}
@@ -186,6 +194,9 @@ public static Stream<?> toStream(Object object) {
186194
if (object instanceof Iterator) {
187195
return stream(spliteratorUnknownSize((Iterator<?>) object, ORDERED), false);
188196
}
197+
if (object instanceof Spliterator) {
198+
return stream((Spliterator<?>) object, false);
199+
}
189200
if (object instanceof Object[]) {
190201
return Arrays.stream((Object[]) object);
191202
}
@@ -201,8 +212,7 @@ public static Stream<?> toStream(Object object) {
201212
if (object.getClass().isArray() && object.getClass().getComponentType().isPrimitive()) {
202213
return IntStream.range(0, Array.getLength(object)).mapToObj(i -> Array.get(object, i));
203214
}
204-
throw new PreconditionViolationException(
205-
"Cannot convert instance of " + object.getClass().getName() + " into a Stream: " + object);
215+
return StreamUtils.tryConvertToStreamByReflection(object);
206216
}
207217

208218
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2015-2023 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+
11+
package org.junit.platform.commons.util;
12+
13+
import static java.util.Spliterator.ORDERED;
14+
import static java.util.Spliterators.spliteratorUnknownSize;
15+
import static java.util.stream.StreamSupport.stream;
16+
17+
import java.lang.reflect.Method;
18+
import java.util.Arrays;
19+
import java.util.Iterator;
20+
import java.util.Optional;
21+
import java.util.Spliterator;
22+
import java.util.stream.Stream;
23+
24+
import org.junit.platform.commons.JUnitException;
25+
import org.junit.platform.commons.PreconditionViolationException;
26+
import org.junit.platform.commons.function.Try;
27+
28+
/**
29+
* Collection of utilities for working with {@link Stream Streams}.
30+
*
31+
* @since 5.10
32+
*/
33+
final class StreamUtils {
34+
35+
private StreamUtils() {
36+
}
37+
38+
static Stream<?> tryConvertToStreamByReflection(Object object) {
39+
Preconditions.notNull(object, "Object must not be null");
40+
Class<?> theClass = object.getClass();
41+
try {
42+
String name = "iterator";
43+
Method method = theClass.getMethod(name);
44+
if (method.getReturnType() == Iterator.class) {
45+
return stream(() -> tryIteratorToSpliterator(object, method), ORDERED, false);
46+
}
47+
else {
48+
throw new IllegalStateException(
49+
"Method with name 'iterator' does not return " + Iterator.class.getName());
50+
}
51+
}
52+
catch (NoSuchMethodException | IllegalStateException e) {
53+
return tryConvertToStreamBySpliterator(object, e);
54+
}
55+
}
56+
57+
private static Stream<?> tryConvertToStreamBySpliterator(Object object, Exception e) {
58+
try {
59+
String name = "spliterator";
60+
Method method = object.getClass().getMethod(name);
61+
if (method.getReturnType() == Spliterator.class) {
62+
return stream(() -> tryInvokeSpliterator(object, method), ORDERED, false);
63+
}
64+
else {
65+
throw new IllegalStateException(
66+
"Method with name '" + name + "' does not return " + Spliterator.class.getName());
67+
}
68+
}
69+
catch (NoSuchMethodException | IllegalStateException ex) {
70+
ex.addSuppressed(e);
71+
return tryConvertByIteratorSpliteratorReturnType(object, ex);
72+
}
73+
}
74+
75+
private static Stream<?> tryConvertByIteratorSpliteratorReturnType(Object object, Exception ex) {
76+
return streamFromSpliteratorSupplier(object)//
77+
.orElseGet(() -> streamFromIteratorSupplier(object)//
78+
.orElseThrow(() -> //
79+
new PreconditionViolationException(//
80+
"Cannot convert instance of " + object.getClass().getName() + " into a Stream: " + object,
81+
ex)));
82+
}
83+
84+
private static Optional<Stream<?>> streamFromSpliteratorSupplier(Object object) {
85+
return Arrays.stream(object.getClass().getMethods())//
86+
.filter(m -> m.getReturnType() == Spliterator.class)//
87+
.findFirst()//
88+
.map(m -> stream(() -> tryInvokeSpliterator(object, m), ORDERED, false));//
89+
}
90+
91+
private static Optional<Stream<?>> streamFromIteratorSupplier(Object object) {
92+
return Arrays.stream(object.getClass().getMethods())//
93+
.filter(m -> m.getReturnType() == Iterator.class)//
94+
.findFirst()//
95+
.map(m -> stream(() -> tryIteratorToSpliterator(object, m), ORDERED, false));//
96+
}
97+
98+
private static Spliterator<?> tryInvokeSpliterator(Object object, Method method) {
99+
return Try.call(() -> (Spliterator<?>) method.invoke(object))//
100+
.getOrThrow(e -> new JUnitException("Cannot invoke method " + method.getName() + " onto " + object, e));//
101+
}
102+
103+
private static Spliterator<?> tryIteratorToSpliterator(Object object, Method method) {
104+
return Try.call(() -> method.invoke(object))//
105+
.andThen(m -> Try.call(() -> spliteratorUnknownSize((Iterator<?>) m, ORDERED)))//
106+
.getOrThrow(e -> new JUnitException("Cannot invoke method " + method.getName() + " onto " + object, e));//
107+
}
108+
109+
}

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

+66-4
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,13 @@
2121

2222
import java.lang.reflect.Array;
2323
import java.util.ArrayList;
24+
import java.util.Arrays;
2425
import java.util.Collection;
2526
import java.util.Iterator;
2627
import java.util.List;
2728
import java.util.Set;
29+
import java.util.Spliterator;
30+
import java.util.Spliterators;
2831
import java.util.concurrent.atomic.AtomicBoolean;
2932
import java.util.stream.DoubleStream;
3033
import java.util.stream.IntStream;
@@ -60,8 +63,9 @@ void getOnlyElementWithNullCollection() {
6063

6164
@Test
6265
void getOnlyElementWithEmptyCollection() {
66+
Set<Object> emptySet = Set.of();
6367
var exception = assertThrows(PreconditionViolationException.class,
64-
() -> CollectionUtils.getOnlyElement(Set.of()));
68+
() -> CollectionUtils.getOnlyElement(emptySet));
6569
assertEquals("collection must contain exactly one element: []", exception.getMessage());
6670
}
6771

@@ -74,8 +78,9 @@ void getOnlyElementWithSingleElementCollection() {
7478

7579
@Test
7680
void getOnlyElementWithMultiElementCollection() {
81+
List<String> strings = List.of("foo", "bar");
7782
var exception = assertThrows(PreconditionViolationException.class,
78-
() -> CollectionUtils.getOnlyElement(List.of("foo", "bar")));
83+
() -> CollectionUtils.getOnlyElement(strings));
7984
assertEquals("collection must contain exactly one element: [foo, bar]", exception.getMessage());
8085
}
8186

@@ -94,6 +99,9 @@ void toUnmodifiableListThrowsOnMutation() {
9499
Collection.class, //
95100
Iterable.class, //
96101
Iterator.class, //
102+
Spliterator.class, //
103+
MySpliteratorProvider.class, //
104+
MyIteratorProvider.class, //
97105
Object[].class, //
98106
String[].class, //
99107
int[].class, //
@@ -118,7 +126,9 @@ static Stream<Object> objectsConvertibleToStreams() {
118126
LongStream.of(100000000), //
119127
Set.of(1, 2, 3), //
120128
Arguments.of((Object) new Object[] { 9, 8, 7 }), //
121-
new int[] { 5, 10, 15 }//
129+
new int[] { 5, 10, 15 }, //
130+
MySpliteratorProvider.of(new String[] { "mouse", "bear" }), //
131+
MyIteratorProvider.of(new Integer[] { 1, 2, 3, 4, 5 })//
122132
);
123133
}
124134

@@ -196,7 +206,7 @@ void toStreamWithLongStream() {
196206
}
197207

198208
@Test
199-
@SuppressWarnings({ "unchecked", "serial" })
209+
@SuppressWarnings({ "unchecked" })
200210
void toStreamWithCollection() {
201211
var collectionStreamClosed = new AtomicBoolean(false);
202212
Collection<String> input = new ArrayList<>() {
@@ -241,6 +251,36 @@ void toStreamWithIterator() {
241251
assertThat(result).containsExactly("foo", "bar");
242252
}
243253

254+
@Test
255+
@SuppressWarnings("unchecked")
256+
void toStreamWithSpliterator() {
257+
final var input = List.of("foo", "bar").spliterator();
258+
259+
final var result = (Stream<String>) CollectionUtils.toStream(input);
260+
261+
assertThat(result).containsExactly("foo", "bar");
262+
}
263+
264+
@Test
265+
@SuppressWarnings("unchecked")
266+
void toStreamWithIteratorProvider() {
267+
final var input = MyIteratorProvider.of(new String[] { "foo", "bar" });
268+
269+
final var result = (Stream<String>) CollectionUtils.toStream(input);
270+
271+
assertThat(result).containsExactly("foo", "bar");
272+
}
273+
274+
@Test
275+
@SuppressWarnings("unchecked")
276+
void toStreamWithSpliteratorProvider() {
277+
final var input = MySpliteratorProvider.of(new String[] { "foo", "bar" });
278+
279+
var result = (Stream<String>) CollectionUtils.toStream(input);
280+
281+
assertThat(result).containsExactly("foo", "bar");
282+
}
283+
244284
@Test
245285
@SuppressWarnings("unchecked")
246286
void toStreamWithArray() {
@@ -304,4 +344,26 @@ public Object convert(Object source, ParameterContext context) throws ArgumentCo
304344
return source == null ? List.of() : List.of(((String) source).split(","));
305345
}
306346
}
347+
348+
@FunctionalInterface
349+
private interface MySpliteratorProvider<T> {
350+
351+
@SuppressWarnings("unused")
352+
Spliterator<T> thisReturnsASpliterator();
353+
354+
static <T> MySpliteratorProvider<T> of(T[] elements) {
355+
return () -> Arrays.spliterator(elements);
356+
}
357+
}
358+
359+
@FunctionalInterface
360+
private interface MyIteratorProvider<T> {
361+
362+
@SuppressWarnings("unused")
363+
Iterator<T> thisReturnsAnIterator();
364+
365+
static <T> MyIteratorProvider<T> of(T[] elements) {
366+
return () -> Spliterators.iterator(Arrays.spliterator(elements));
367+
}
368+
}
307369
}

0 commit comments

Comments
 (0)