Skip to content

Commit

Permalink
Kotlin Sequence support for @testfactory
Browse files Browse the repository at this point in the history
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.
```
  • Loading branch information
Hans Zuidervaart committed Jul 2, 2023
1 parent 1f36bc8 commit 7cb201b
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2015-2023 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
*
* https://www.eclipse.org/legal/epl-v20.html
*/
package org.junit.jupiter.api

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.DynamicTest.dynamicTest
import java.math.BigDecimal
import java.math.BigDecimal.ONE
import java.math.MathContext
import java.math.BigInteger as BigInt
import java.math.RoundingMode as Rounding

/**
* Unit tests for JUnit Jupiter [TestFactory] use in kotlin classes.
*
* @since 5.10
*/
class KotlinDynamicTests {

@Nested
inner class SequenceReturningTestFactoryTests {

@TestFactory
fun `dynamic tests returned as Kotlin sequence`() = generateSequence(0) { it + 2 }
.map { dynamicTest("$it should be even") { assertTrue(it % 2 == 0) } }
.take(10)

@TestFactory
fun `is anagram tests`(): Sequence<DynamicTest> {
infix fun CharSequence.isAngramOf(other: CharSequence) = groupBy { it } == other.groupBy { it }

infix fun CharSequence.`should be an anagram of`(other: CharSequence) =
dynamicTest("'$this' should be an anagram of '$other'") { assertTrue(this isAngramOf other) }

infix fun CharSequence.`should not be an anagram of`(other: CharSequence) =
dynamicTest("'$this' should not be an anagram of '$other'") { assertFalse(this isAngramOf other) }

return sequenceOf(
"a gentleman" `should be an anagram of` "elegant man",
"laptop machines" `should be an anagram of` "apple macintosh",
"salvador dali" `should be an anagram of` "avida dollars",
"a gentleman" `should not be an anagram of` "spider man",
"laptop computers" `should not be an anagram of` "apple macintosh",
"salvador dali" `should not be an anagram of` "picasso"
)
}

@TestFactory
fun `Consecutive fibonacci nr ratios, should converge to golden ratio as n increases`(): Sequence<DynamicTest> {
val scale = 5
val goldenRatio = (ONE + 5.toBigDecimal().sqrt(MathContext(scale + 10, Rounding.HALF_UP)))
.divide(2.toBigDecimal(), scale, Rounding.HALF_UP)

fun shouldApproximateGoldenRatio(cur: BigDecimal, next: BigDecimal) =
next.divide(cur, scale, Rounding.HALF_UP).let {
dynamicTest("$cur / $next = $it should approximate the golden ratio in $scale decimals") {
assertEquals(goldenRatio, it)
}
}
return generateSequence(BigInt.ONE to BigInt.ONE) { (cur, next) -> next to cur + next }
.map { (cur) -> cur.toBigDecimal() }
.zipWithNext(::shouldApproximateGoldenRatio)
.drop(14)
.take(10)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static org.apiguardian.api.API.Status.INTERNAL;

import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
Expand All @@ -26,6 +27,7 @@
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import java.util.Spliterator;
import java.util.function.Consumer;
import java.util.stream.Collector;
import java.util.stream.DoubleStream;
Expand Down Expand Up @@ -99,7 +101,7 @@ public static <T> Set<T> toSet(T[] values) {
* returned, so if more control over the returned list is required,
* consider creating a new {@code Collector} implementation like the
* following:
*
* <p>
* <pre class="code">
* public static &lt;T&gt; Collector&lt;T, ?, List&lt;T&gt;&gt; toUnmodifiableList(Supplier&lt;List&lt;T&gt;&gt; listSupplier) {
* return Collectors.collectingAndThen(Collectors.toCollection(listSupplier), Collections::unmodifiableList);
Expand Down Expand Up @@ -138,8 +140,12 @@ public static boolean isConvertibleToStream(Class<?> type) {
|| LongStream.class.isAssignableFrom(type)//
|| Iterable.class.isAssignableFrom(type)//
|| Iterator.class.isAssignableFrom(type)//
|| Spliterator.class.isAssignableFrom(type)//
|| Object[].class.isAssignableFrom(type)//
|| (type.isArray() && type.getComponentType().isPrimitive()));
|| (type.isArray() && type.getComponentType().isPrimitive())//
|| Arrays.stream(type.getMethods())//
.map(Method::getReturnType)//
.anyMatch(returnType -> returnType == Iterator.class || returnType == Spliterator.class));
}

/**
Expand All @@ -153,8 +159,10 @@ public static boolean isConvertibleToStream(Class<?> type) {
* <li>{@link Collection}</li>
* <li>{@link Iterable}</li>
* <li>{@link Iterator}</li>
* <li>{@link Spliterator}</li>
* <li>{@link Object} array</li>
* <li>primitive array</li>
* <li>An object that contains an iterator or spliterator returning method</li>
* </ul>
*
* @param object the object to convert into a stream; never {@code null}
Expand Down Expand Up @@ -186,6 +194,9 @@ public static Stream<?> toStream(Object object) {
if (object instanceof Iterator) {
return stream(spliteratorUnknownSize((Iterator<?>) object, ORDERED), false);
}
if (object instanceof Spliterator) {
return stream((Spliterator<?>) object, false);
}
if (object instanceof Object[]) {
return Arrays.stream((Object[]) object);
}
Expand All @@ -201,8 +212,7 @@ public static Stream<?> toStream(Object object) {
if (object.getClass().isArray() && object.getClass().getComponentType().isPrimitive()) {
return IntStream.range(0, Array.getLength(object)).mapToObj(i -> Array.get(object, i));
}
throw new PreconditionViolationException(
"Cannot convert instance of " + object.getClass().getName() + " into a Stream: " + object);
return StreamUtils.tryConvertToStreamByIterator(object);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright 2015-2023 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
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.platform.commons.util;

import static java.util.Spliterator.ORDERED;
import static java.util.Spliterators.spliteratorUnknownSize;
import static java.util.stream.StreamSupport.stream;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Optional;
import java.util.Spliterator;
import java.util.stream.Stream;

import org.junit.platform.commons.JUnitException;
import org.junit.platform.commons.PreconditionViolationException;

/**
* Collection of utilities for working with {@link Stream Streams}.
*
* @since 5.10
*/
final class StreamUtils {

private StreamUtils() {
}

static Stream<?> tryConvertToStreamByIterator(Object object) {
Class<?> theClass = object.getClass();
try {
Method method = theClass.getMethod("iterator");
return stream(() -> tryIteratorToSpliterator(object, method), ORDERED, false);
}
catch (NoSuchMethodException e) {
return tryConvertToStreamBySpliterator(object);
}
}

private static Stream<?> tryConvertToStreamBySpliterator(Object object) {
try {
Method method = object.getClass().getMethod("spliterator");
return stream(() -> tryInvokeSpliterator(object, method), ORDERED, false);
}
catch (NoSuchMethodException ex) {
return tryConvertByIteratorSpliteratorReturnType(object);
}
}

private static Stream<?> tryConvertByIteratorSpliteratorReturnType(Object object) {
Optional<? extends Stream<?>> stream = streamFromSpliteratorSupplier(object);
// is necessary because orElseGet gives generics compile errors
return stream.isPresent() ? stream.get()
: streamFromIteratorSupplier(object).orElseThrow(() -> new PreconditionViolationException(
"Cannot convert instance of " + object.getClass().getName() + " into a Stream: " + object));

}

private static Optional<? extends Stream<?>> streamFromSpliteratorSupplier(Object object) {
return Arrays.stream(object.getClass().getMethods()).filter(
m -> m.getReturnType() == Spliterator.class).findFirst().map(
m -> stream(() -> tryInvokeSpliterator(object, m), ORDERED, false));
}

private static Optional<Stream<?>> streamFromIteratorSupplier(Object object) {
return Arrays.stream(object.getClass().getMethods()).filter(
m -> m.getReturnType() == Iterator.class).findFirst().map(
m -> stream(() -> tryIteratorToSpliterator(object, m), ORDERED, false));
}

private static Spliterator<?> tryInvokeSpliterator(Object object, Method method) {
try {
return (Spliterator<?>) method.invoke(object);
}
catch (IllegalAccessException | InvocationTargetException | ClassCastException ex) {
throw new JUnitException("Cannot invoke method " + method.getName() + " onto " + object);
}
}

private static Spliterator<?> tryIteratorToSpliterator(Object object, Method method) {
try {
return spliteratorUnknownSize((Iterator<?>) method.invoke(object), ORDERED);
}
catch (IllegalAccessException | InvocationTargetException | ClassCastException e) {
throw new JUnitException("Cannot invoke method " + method.getName() + " onto " + object);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
Expand Down Expand Up @@ -60,8 +63,9 @@ void getOnlyElementWithNullCollection() {

@Test
void getOnlyElementWithEmptyCollection() {
Set<Object> emptySet = Set.of();
var exception = assertThrows(PreconditionViolationException.class,
() -> CollectionUtils.getOnlyElement(Set.of()));
() -> CollectionUtils.getOnlyElement(emptySet));
assertEquals("collection must contain exactly one element: []", exception.getMessage());
}

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

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

Expand All @@ -94,6 +99,9 @@ void toUnmodifiableListThrowsOnMutation() {
Collection.class, //
Iterable.class, //
Iterator.class, //
Spliterator.class,//
MySpliteratorProvider.class,//
MyIteratorProvider.class,//
Object[].class, //
String[].class, //
int[].class, //
Expand All @@ -118,7 +126,9 @@ static Stream<Object> objectsConvertibleToStreams() {
LongStream.of(100000000), //
Set.of(1, 2, 3), //
Arguments.of((Object) new Object[] { 9, 8, 7 }), //
new int[] { 5, 10, 15 }//
new int[] { 5, 10, 15 }, //
MySpliteratorProvider.of(new String[] { "mouse", "bear" }), //
MyIteratorProvider.of(new Integer[] { 1, 2, 3, 4, 5 })//
);
}

Expand Down Expand Up @@ -196,7 +206,7 @@ void toStreamWithLongStream() {
}

@Test
@SuppressWarnings({ "unchecked", "serial" })
@SuppressWarnings({ "unchecked"})
void toStreamWithCollection() {
var collectionStreamClosed = new AtomicBoolean(false);
Collection<String> input = new ArrayList<>() {
Expand Down Expand Up @@ -241,6 +251,36 @@ void toStreamWithIterator() {
assertThat(result).containsExactly("foo", "bar");
}

@Test
@SuppressWarnings("unchecked")
void toStreamWithSpliterator() {
final var input = List.of("foo", "bar").spliterator();

final var result = (Stream<String>) CollectionUtils.toStream(input);

assertThat(result).containsExactly("foo", "bar");
}

@Test
@SuppressWarnings("unchecked")
void toStreamWithIteratorProvider() {
final var input = MyIteratorProvider.of(new String[] {"foo", "bar"});

final var result = (Stream<String>) CollectionUtils.toStream(input);

assertThat(result).containsExactly("foo", "bar");
}

@Test
@SuppressWarnings("unchecked")
void toStreamWithSpliteratorProvider() {
final var input = MySpliteratorProvider.of(new String[] {"foo", "bar"});

var result = (Stream<String>) CollectionUtils.toStream(input);

assertThat(result).containsExactly("foo", "bar");
}

@Test
@SuppressWarnings("unchecked")
void toStreamWithArray() {
Expand Down Expand Up @@ -304,4 +344,26 @@ public Object convert(Object source, ParameterContext context) throws ArgumentCo
return source == null ? List.of() : List.of(((String) source).split(","));
}
}

@FunctionalInterface
private interface MySpliteratorProvider<T> {

@SuppressWarnings("unused")
Spliterator<T> thisReturnsASpliterator();

static <T> MySpliteratorProvider<T> of(T[] elements) {
return () -> Arrays.spliterator(elements);
}
}

@FunctionalInterface
private interface MyIteratorProvider<T> {

@SuppressWarnings("unused")
Iterator<T> thisReturnsAnIterator();

static <T> MyIteratorProvider<T> of(T[] elements) {
return () -> Spliterators.iterator(Arrays.spliterator(elements));
}
}
}

0 comments on commit 7cb201b

Please sign in to comment.