diff --git a/junit-jupiter-engine/src/test/kotlin/org/junit/jupiter/api/KotlinDynamicTests.kt b/junit-jupiter-engine/src/test/kotlin/org/junit/jupiter/api/KotlinDynamicTests.kt new file mode 100644 index 000000000000..df0022b828ef --- /dev/null +++ b/junit-jupiter-engine/src/test/kotlin/org/junit/jupiter/api/KotlinDynamicTests.kt @@ -0,0 +1,56 @@ +/* + * 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 `Consecutive fibonacci nr ratios, should converge to golden ratio as n increases`(): Sequence { + 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) + } + } +} diff --git a/junit-jupiter-params/src/test/kotlin/org/junit/jupiter/params/aggregator/KotlinParameterizedTests.kt b/junit-jupiter-params/src/test/kotlin/org/junit/jupiter/params/aggregator/KotlinParameterizedTests.kt new file mode 100644 index 000000000000..4273083bb580 --- /dev/null +++ b/junit-jupiter-params/src/test/kotlin/org/junit/jupiter/params/aggregator/KotlinParameterizedTests.kt @@ -0,0 +1,37 @@ +/* + * 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.params.aggregator + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments.arguments +import org.junit.jupiter.params.provider.MethodSource +import java.time.Month + +/** + * Tests for ParameterizedTest kotlin compatibility + */ +object KotlinParameterizedTests { + + @ParameterizedTest + @MethodSource("dataProvidedByKotlinSequence") + fun `a method source can be supplied by a Sequence returning method`(value: Int, month: Month) { + assertEquals(value, month.value) + } + + @JvmStatic + private fun dataProvidedByKotlinSequence() = sequenceOf( + arguments(1, Month.JANUARY), + arguments(3, Month.MARCH), + arguments(8, Month.AUGUST), + arguments(5, Month.MAY), + arguments(12, Month.DECEMBER) + ) +} diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/CollectionUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/CollectionUtils.java index fa70025bda45..193d0bcecb73 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/CollectionUtils.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/CollectionUtils.java @@ -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; @@ -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; @@ -34,7 +36,9 @@ import java.util.stream.Stream; import org.apiguardian.api.API; +import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.PreconditionViolationException; +import org.junit.platform.commons.function.Try; /** * Collection of utilities for working with {@link Collection Collections}. @@ -99,7 +103,7 @@ public static Set toSet(T[] values) { * returned, so if more control over the returned list is required, * consider creating a new {@code Collector} implementation like the * following: - * + *

*

 	 * public static <T> Collector<T, ?, List<T>> toUnmodifiableList(Supplier<List<T>> listSupplier) {
 	 *     return Collectors.collectingAndThen(Collectors.toCollection(listSupplier), Collections::unmodifiableList);
@@ -139,7 +143,11 @@ public static boolean isConvertibleToStream(Class type) {
 				|| Iterable.class.isAssignableFrom(type)//
 				|| Iterator.class.isAssignableFrom(type)//
 				|| Object[].class.isAssignableFrom(type)//
-				|| (type.isArray() && type.getComponentType().isPrimitive()));
+				|| (type.isArray() && type.getComponentType().isPrimitive())//
+				|| Arrays.stream(type.getMethods())//
+						.filter(m -> m.getName().equals("iterator"))//
+						.map(Method::getReturnType)//
+						.anyMatch(returnType -> returnType == Iterator.class));
 	}
 
 	/**
@@ -155,6 +163,7 @@ public static boolean isConvertibleToStream(Class type) {
 	 * 
  • {@link Iterator}
  • *
  • {@link Object} array
  • *
  • primitive array
  • + *
  • An object that contains a method with name `iterator` returning an Iterator object
  • * * * @param object the object to convert into a stream; never {@code null} @@ -201,8 +210,31 @@ 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 tryConvertToStreamByReflection(object); + } + + private static Stream tryConvertToStreamByReflection(Object object) { + Preconditions.notNull(object, "Object must not be null"); + try { + String name = "iterator"; + Method method = object.getClass().getMethod(name); + if (method.getReturnType() == Iterator.class) { + return stream(() -> tryIteratorToSpliterator(object, method), ORDERED, false); + } + else { + throw new PreconditionViolationException( + "Method with name 'iterator' does not return " + Iterator.class.getName()); + } + } + catch (NoSuchMethodException | IllegalStateException e) { + throw new PreconditionViolationException(// + "Cannot convert instance of " + object.getClass().getName() + " into a Stream: " + object, e); + } + } + + private static Spliterator tryIteratorToSpliterator(Object object, Method method) { + return Try.call(() -> spliteratorUnknownSize((Iterator) method.invoke(object), ORDERED))// + .getOrThrow(e -> new JUnitException("Cannot invoke method " + method.getName() + " onto " + object, e));// } /** diff --git a/platform-tests/src/test/java/org/junit/platform/commons/util/CollectionUtilsTests.java b/platform-tests/src/test/java/org/junit/platform/commons/util/CollectionUtilsTests.java index a925b75a1388..1c83771a2dfc 100644 --- a/platform-tests/src/test/java/org/junit/platform/commons/util/CollectionUtilsTests.java +++ b/platform-tests/src/test/java/org/junit/platform/commons/util/CollectionUtilsTests.java @@ -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; @@ -94,6 +97,7 @@ void toUnmodifiableListThrowsOnMutation() { Collection.class, // Iterable.class, // Iterator.class, // + IteratorProvider.class, // Object[].class, // String[].class, // int[].class, // @@ -115,10 +119,11 @@ static Stream objectsConvertibleToStreams() { Stream.of("cat", "dog"), // DoubleStream.of(42.3), // IntStream.of(99), // - LongStream.of(100000000), // + LongStream.of(100_000_000), // Set.of(1, 2, 3), // Arguments.of((Object) new Object[] { 9, 8, 7 }), // - new int[] { 5, 10, 15 }// + new int[] { 5, 10, 15 }, // + IteratorProvider.of(new Integer[] { 1, 2, 3, 4, 5 })// ); } @@ -129,6 +134,8 @@ static Stream objectsConvertibleToStreams() { Object.class, // Integer.class, // String.class, // + IteratorProviderNotUsable.class, // + Spliterator.class, // int.class, // boolean.class // }) @@ -196,7 +203,7 @@ void toStreamWithLongStream() { } @Test - @SuppressWarnings({ "unchecked", "serial" }) + @SuppressWarnings({ "unchecked" }) void toStreamWithCollection() { var collectionStreamClosed = new AtomicBoolean(false); Collection input = new ArrayList<>() { @@ -241,6 +248,24 @@ void toStreamWithIterator() { assertThat(result).containsExactly("foo", "bar"); } + @Test + @SuppressWarnings("unchecked") + void toStreamWithIteratorProvider() { + final var input = IteratorProvider.of(new String[] { "foo", "bar" }); + + final var result = (Stream) CollectionUtils.toStream(input); + + assertThat(result).containsExactly("foo", "bar"); + } + + @Test + void throwWhenIteratorNamedMethodDoesNotReturnAnIterator() { + var o = IteratorProviderNotUsable.of(new String[] { "Test" }); + var e = assertThrows(PreconditionViolationException.class, () -> CollectionUtils.toStream(o)); + + assertEquals("Method with name 'iterator' does not return java.util.Iterator", e.getMessage()); + } + @Test @SuppressWarnings("unchecked") void toStreamWithArray() { @@ -304,4 +329,29 @@ public Object convert(Object source, ParameterContext context) throws ArgumentCo return source == null ? List.of() : List.of(((String) source).split(",")); } } + + /** + * An interface that has a method with name 'iterator', returning a java.util/Iterator as a return type + */ + private interface IteratorProvider { + + @SuppressWarnings("unused") + Iterator iterator(); + + static IteratorProvider of(T[] elements) { + return () -> Spliterators.iterator(Arrays.spliterator(elements)); + } + } + + /** + * An interface that has a method with name 'iterator', but does not return java.util/Iterator as a return type + */ + private interface IteratorProviderNotUsable { + @SuppressWarnings("unused") + Object iterator(); + + static IteratorProviderNotUsable of(T[] elements) { + return () -> Spliterators.iterator(Arrays.spliterator(elements)); + } + } }