From 46fe6601811bb468bf6137a6b148ad6a2c538257 Mon Sep 17 00:00:00 2001 From: Hans Zuidervaart <hans@drillster.com> Date: Sun, 2 Jul 2023 13:31:05 +0200 Subject: [PATCH 1/7] 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. --- .../asciidoc/user-guide/writing-tests.adoc | 2 +- .../descriptor/TestFactoryTestDescriptor.java | 2 +- .../commons/util/CollectionUtils.java | 40 +++++++++++-- .../junit/jupiter/api/KotlinDynamicTests.kt | 56 +++++++++++++++++++ .../aggregator/KotlinParameterizedTests.kt | 40 +++++++++++++ .../commons/util/CollectionUtilsTests.java | 55 +++++++++++++++++- 6 files changed, 186 insertions(+), 9 deletions(-) create mode 100644 jupiter-tests/src/test/kotlin/org/junit/jupiter/api/KotlinDynamicTests.kt create mode 100644 jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/KotlinParameterizedTests.kt diff --git a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc index 8442461ae2e1..23c278e8641f 100644 --- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc @@ -2480,7 +2480,7 @@ generated at runtime by a factory method that is annotated with `@TestFactory`. In contrast to `@Test` methods, a `@TestFactory` method is not itself a test case but rather a factory for test cases. Thus, a dynamic test is the product of a factory. Technically speaking, a `@TestFactory` method must return a single `DynamicNode` or a -`Stream`, `Collection`, `Iterable`, `Iterator`, or array of `DynamicNode` instances. +`Stream`, `Collection`, `Iterable`, `Iterator`, an `Iterator` providing class or array of `DynamicNode` instances. Instantiable subclasses of `DynamicNode` are `DynamicContainer` and `DynamicTest`. `DynamicContainer` instances are composed of a _display name_ and a list of dynamic child nodes, enabling the creation of arbitrarily nested hierarchies of dynamic nodes. diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptor.java index f0d37814bbf2..15e029ca6b16 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptor.java @@ -132,7 +132,7 @@ private Stream<DynamicNode> toDynamicNodeStream(Object testFactoryMethodResult) private JUnitException invalidReturnTypeException(Throwable cause) { String message = String.format( - "@TestFactory method [%s] must return a single %2$s or a Stream, Collection, Iterable, Iterator, or array of %2$s.", + "@TestFactory method [%s] must return a single %2$s or a Stream, Collection, Iterable, Iterator, Iterator-source or array of %2$s.", getTestMethod().toGenericString(), DynamicNode.class.getName()); return new JUnitException(message, cause); } 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 e12d0421f286..920056b235fe 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; @@ -27,6 +28,7 @@ import java.util.ListIterator; import java.util.Optional; import java.util.Set; +import java.util.Spliterator; import java.util.function.Consumer; import java.util.stream.Collector; import java.util.stream.DoubleStream; @@ -35,7 +37,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}. @@ -122,7 +126,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 <T> Collector<T, ?, List<T>> toUnmodifiableList(Supplier<List<T>> listSupplier) { * return Collectors.collectingAndThen(Collectors.toCollection(listSupplier), Collections::unmodifiableList); @@ -161,7 +165,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)); } /** @@ -177,6 +185,7 @@ public static boolean isConvertibleToStream(Class<?> type) { * <li>{@link Iterator}</li> * <li>{@link Object} array</li> * <li>primitive array</li> + * <li>An object that contains a method with name `iterator` returning an Iterator object</li> * </ul> * * @param object the object to convert into a stream; never {@code null} @@ -223,8 +232,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/jupiter-tests/src/test/kotlin/org/junit/jupiter/api/KotlinDynamicTests.kt b/jupiter-tests/src/test/kotlin/org/junit/jupiter/api/KotlinDynamicTests.kt new file mode 100644 index 000000000000..ba0dec561903 --- /dev/null +++ b/jupiter-tests/src/test/kotlin/org/junit/jupiter/api/KotlinDynamicTests.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2015-2025 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.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.12 + */ +class KotlinDynamicTests { + @Nested + inner class SequenceReturningTestFactoryTests { + @TestFactory + fun `Dynamic tests returned as Kotlin sequence`() = + generateSequence(0) { it + 2 } + .map { dynamicTest("$it should be even") { assertEquals(0, it % 2) } } + .take(10) + + @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) + } + } +} diff --git a/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/KotlinParameterizedTests.kt b/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/KotlinParameterizedTests.kt new file mode 100644 index 000000000000..464f2940c1b9 --- /dev/null +++ b/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/KotlinParameterizedTests.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2015-2025 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/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 9e6f01daccbb..23adce41d1a5 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 @@ -25,6 +25,8 @@ 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; @@ -139,6 +141,7 @@ class StreamConversion { Collection.class, // Iterable.class, // Iterator.class, // + IteratorProvider.class, // Object[].class, // String[].class, // int[].class, // @@ -160,10 +163,11 @@ static Stream<Object> 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 })// ); } @@ -174,6 +178,8 @@ static Stream<Object> objectsConvertibleToStreams() { Object.class, // Integer.class, // String.class, // + IteratorProviderNotUsable.class, // + Spliterator.class, // int.class, // boolean.class // }) @@ -242,7 +248,7 @@ void toStreamWithLongStream() { } @Test - @SuppressWarnings({ "unchecked", "serial" }) + @SuppressWarnings({ "unchecked" }) void toStreamWithCollection() { var collectionStreamClosed = new AtomicBoolean(false); Collection<String> input = new ArrayList<>() { @@ -287,6 +293,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<String>) 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() { @@ -355,4 +379,29 @@ public Object convert(Object source, ParameterContext context) throws ArgumentCo } } } + + /** + * An interface that has a method with name 'iterator', returning a java.util/Iterator as a return type + */ + private interface IteratorProvider<T> { + + @SuppressWarnings("unused") + Iterator<T> iterator(); + + static <T> IteratorProvider<T> 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 <T> IteratorProviderNotUsable of(T[] elements) { + return () -> Spliterators.iterator(Arrays.spliterator(elements)); + } + } } From e3df4aea355f82ee78d73226384d96594458f31b Mon Sep 17 00:00:00 2001 From: Marc Philipp <mail@marcphilipp.de> Date: Tue, 6 May 2025 09:38:25 +0200 Subject: [PATCH 2/7] Polish implementation and tests --- .../commons/util/CollectionUtils.java | 42 +++++++------------ .../commons/util/CollectionUtilsTests.java | 39 +++++++---------- 2 files changed, 30 insertions(+), 51 deletions(-) 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 920056b235fe..6f250e532e76 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 @@ -16,6 +16,7 @@ import static java.util.stream.Collectors.toList; import static java.util.stream.StreamSupport.stream; import static org.apiguardian.api.API.Status.INTERNAL; +import static org.junit.platform.commons.support.ReflectionSupport.invokeMethod; import java.lang.reflect.Array; import java.lang.reflect.Method; @@ -28,7 +29,6 @@ import java.util.ListIterator; import java.util.Optional; import java.util.Set; -import java.util.Spliterator; import java.util.function.Consumer; import java.util.stream.Collector; import java.util.stream.DoubleStream; @@ -37,9 +37,8 @@ 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; +import org.junit.platform.commons.support.ReflectionSupport; /** * Collection of utilities for working with {@link Collection Collections}. @@ -166,10 +165,7 @@ public static boolean isConvertibleToStream(Class<?> type) { || Iterator.class.isAssignableFrom(type)// || Object[].class.isAssignableFrom(type)// || (type.isArray() && type.getComponentType().isPrimitive())// - || Arrays.stream(type.getMethods())// - .filter(m -> m.getName().equals("iterator"))// - .map(Method::getReturnType)// - .anyMatch(returnType -> returnType == Iterator.class)); + || findIteratorMethod(type).isPresent()); } /** @@ -185,7 +181,9 @@ public static boolean isConvertibleToStream(Class<?> type) { * <li>{@link Iterator}</li> * <li>{@link Object} array</li> * <li>primitive array</li> - * <li>An object that contains a method with name `iterator` returning an Iterator object</li> + * <li>any type that provides an + * {@link java.util.Iterator Iterator}-returning {@code iterator()} method + * (such as, for example, a {@code kotlin.sequences.Sequence})</li> * </ul> * * @param object the object to convert into a stream; never {@code null} @@ -236,27 +234,17 @@ public static Stream<?> toStream(Object 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); - } + return findIteratorMethod(object.getClass()) // + .map(method -> (Iterator<?>) invokeMethod(method, object)) // + .map(iterator -> spliteratorUnknownSize(iterator, ORDERED)) // + .map(spliterator -> stream(spliterator, false)) // + .orElseThrow(() -> new PreconditionViolationException(String.format( + "Cannot convert instance of %s into a Stream: %s", object.getClass().getName(), object))); } - 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));// + private static Optional<Method> findIteratorMethod(Class<?> type) { + return ReflectionSupport.findMethod(type, "iterator") // + .filter(method -> method.getReturnType() == Iterator.class); } /** 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 23adce41d1a5..749f62161073 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 @@ -26,7 +26,6 @@ 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; @@ -167,7 +166,7 @@ static Stream<Object> objectsConvertibleToStreams() { Set.of(1, 2, 3), // Arguments.of((Object) new Object[] { 9, 8, 7 }), // new int[] { 5, 10, 15 }, // - IteratorProvider.of(new Integer[] { 1, 2, 3, 4, 5 })// + new IteratorProvider(1, 2, 3, 4, 5)// ); } @@ -178,7 +177,7 @@ static Stream<Object> objectsConvertibleToStreams() { Object.class, // Integer.class, // String.class, // - IteratorProviderNotUsable.class, // + UnusableIteratorProvider.class, // Spliterator.class, // int.class, // boolean.class // @@ -251,13 +250,7 @@ void toStreamWithLongStream() { @SuppressWarnings({ "unchecked" }) void toStreamWithCollection() { var collectionStreamClosed = new AtomicBoolean(false); - Collection<String> input = new ArrayList<>() { - - { - add("foo"); - add("bar"); - } - + var input = new ArrayList<>(List.of("foo", "bar")) { @Override public Stream<String> stream() { return super.stream().onClose(() -> collectionStreamClosed.set(true)); @@ -296,19 +289,20 @@ void toStreamWithIterator() { @Test @SuppressWarnings("unchecked") void toStreamWithIteratorProvider() { - final var input = IteratorProvider.of(new String[] { "foo", "bar" }); + var input = new IteratorProvider("foo", "bar"); - final var result = (Stream<String>) CollectionUtils.toStream(input); + var result = (Stream<String>) CollectionUtils.toStream(input); assertThat(result).containsExactly("foo", "bar"); } @Test void throwWhenIteratorNamedMethodDoesNotReturnAnIterator() { - var o = IteratorProviderNotUsable.of(new String[] { "Test" }); + var o = new UnusableIteratorProvider("Test"); var e = assertThrows(PreconditionViolationException.class, () -> CollectionUtils.toStream(o)); - assertEquals("Method with name 'iterator' does not return java.util.Iterator", e.getMessage()); + assertEquals("Cannot convert instance of %s into a Stream: %s".formatted( + UnusableIteratorProvider.class.getName(), o), e.getMessage()); } @Test @@ -383,25 +377,22 @@ public Object convert(Object source, ParameterContext context) throws ArgumentCo /** * An interface that has a method with name 'iterator', returning a java.util/Iterator as a return type */ - private interface IteratorProvider<T> { + private record IteratorProvider(Object... elements) { @SuppressWarnings("unused") - Iterator<T> iterator(); - - static <T> IteratorProvider<T> of(T[] elements) { - return () -> Spliterators.iterator(Arrays.spliterator(elements)); + Iterator<?> iterator() { + return Arrays.stream(elements).iterator(); } } /** * 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(); + private record UnusableIteratorProvider(Object... elements) { - static <T> IteratorProviderNotUsable of(T[] elements) { - return () -> Spliterators.iterator(Arrays.spliterator(elements)); + @SuppressWarnings("unused") + Object iterator() { + return Arrays.stream(elements).iterator(); } } } From ebb4a8ad1771b89606e4568818adb2f9b67558f6 Mon Sep 17 00:00:00 2001 From: Marc Philipp <mail@marcphilipp.de> Date: Tue, 6 May 2025 09:38:37 +0200 Subject: [PATCH 3/7] Add test for `@FieldSource` --- ...izedTestKotlinSequenceIntegrationTests.kt} | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) rename jupiter-tests/src/test/kotlin/org/junit/jupiter/params/{aggregator/KotlinParameterizedTests.kt => ParameterizedTestKotlinSequenceIntegrationTests.kt} (55%) diff --git a/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/KotlinParameterizedTests.kt b/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/ParameterizedTestKotlinSequenceIntegrationTests.kt similarity index 55% rename from jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/KotlinParameterizedTests.kt rename to jupiter-tests/src/test/kotlin/org/junit/jupiter/params/ParameterizedTestKotlinSequenceIntegrationTests.kt index 464f2940c1b9..800407845e11 100644 --- a/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/KotlinParameterizedTests.kt +++ b/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/ParameterizedTestKotlinSequenceIntegrationTests.kt @@ -7,21 +7,21 @@ * * https://www.eclipse.org/legal/epl-v20.html */ -package org.junit.jupiter.params.aggregator +package org.junit.jupiter.params 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.FieldSource import org.junit.jupiter.params.provider.MethodSource import java.time.Month /** - * Tests for ParameterizedTest kotlin compatibility + * Tests for Kotlin compatibility of ParameterizedTest */ -object KotlinParameterizedTests { +object ParameterizedTestKotlinSequenceIntegrationTests { @ParameterizedTest - @MethodSource("dataProvidedByKotlinSequence") - fun `a method source can be supplied by a Sequence returning method`( + @MethodSource("dataProvidedByKotlinSequenceMethod") + fun `a method source can be supplied by a Sequence-returning method`( value: Int, month: Month ) { @@ -29,7 +29,10 @@ object KotlinParameterizedTests { } @JvmStatic - private fun dataProvidedByKotlinSequence() = + private fun dataProvidedByKotlinSequenceMethod() = dataProvidedByKotlinSequenceField + + @JvmStatic + val dataProvidedByKotlinSequenceField = sequenceOf( arguments(1, Month.JANUARY), arguments(3, Month.MARCH), @@ -37,4 +40,13 @@ object KotlinParameterizedTests { arguments(5, Month.MAY), arguments(12, Month.DECEMBER) ) + + @ParameterizedTest + @FieldSource("dataProvidedByKotlinSequenceField") + fun `a field source can be supplied by a Sequence-typed field`( + value: Int, + month: Month + ) { + assertEquals(value, month.value) + } } From b089fff12989b19adba4540dd608ffd77bbc9e4d Mon Sep 17 00:00:00 2001 From: Marc Philipp <mail@marcphilipp.de> Date: Tue, 6 May 2025 09:38:50 +0200 Subject: [PATCH 4/7] Document in User Guide --- .../asciidoc/user-guide/writing-tests.adoc | 22 ++++++++++++------- .../org/junit/jupiter/api/TestFactory.java | 4 +++- .../jupiter/params/provider/FieldSource.java | 12 +++++----- .../jupiter/params/provider/MethodSource.java | 13 ++++++----- 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc index 23c278e8641f..34f900b09ca2 100644 --- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc @@ -1639,9 +1639,10 @@ of the annotated `@ParameterizedTest` method. Generally speaking this translates `Stream` of `Arguments` (i.e., `Stream<Arguments>`); however, the actual concrete return type can take on many forms. In this context, a "stream" is anything that JUnit can reliably convert into a `Stream`, such as `Stream`, `DoubleStream`, `LongStream`, -`IntStream`, `Collection`, `Iterator`, `Iterable`, an array of objects, or an array of -primitives. The "arguments" within the stream can be supplied as an instance of -`Arguments`, an array of objects (e.g., `Object[]`), or a single value if the +`IntStream`, `Collection`, `Iterator`, `Iterable`, an array of objects or primitives, or +any type that provides an `iterator(): Iterator` method (such as, for example, a +`kotlin.sequences.Sequence`). The "arguments" within the stream can be supplied as an +instance of `Arguments`, an array of objects (e.g., `Object[]`), or a single value if the parameterized test method accepts a single argument. If you only need a single parameter, you can return a `Stream` of instances of the @@ -1723,10 +1724,11 @@ In this context, a "stream" is anything that JUnit can reliably convert to a `St however, the actual concrete field type can take on many forms. Generally speaking this translates to a `Collection`, an `Iterable`, a `Supplier` of a stream (`Stream`, `DoubleStream`, `LongStream`, or `IntStream`), a `Supplier` of an `Iterator`, an array of -objects, or an array of primitives. Each set of "arguments" within the "stream" can be -supplied as an instance of `Arguments`, an array of objects (for example, `Object[]`, -`String[]`, etc.), or a single value if the parameterized test method accepts a single -argument. +objects or primitives, or any type that provides an `iterator(): Iterator` method (such +as, for example, a `kotlin.sequences.Sequence`). Each set of "arguments" within the +"stream" can be supplied as an instance of `Arguments`, an array of objects (for example, +`Object[]`, `String[]`, etc.), or a single value if the parameterized test method accepts +a single argument. [WARNING] ==== @@ -2480,7 +2482,11 @@ generated at runtime by a factory method that is annotated with `@TestFactory`. In contrast to `@Test` methods, a `@TestFactory` method is not itself a test case but rather a factory for test cases. Thus, a dynamic test is the product of a factory. Technically speaking, a `@TestFactory` method must return a single `DynamicNode` or a -`Stream`, `Collection`, `Iterable`, `Iterator`, an `Iterator` providing class or array of `DynamicNode` instances. +_stream_ of `DynamicNode` instances or any of its subclasses. In this context, a "stream" +is anything that JUnit can reliably convert into a `Stream`, such as `Stream`, +`Collection`, `Iterator`, `Iterable`, an array of objects, or any type that provides an +`iterator(): Iterator` method (such as, for example, a `kotlin.sequences.Sequence`). + Instantiable subclasses of `DynamicNode` are `DynamicContainer` and `DynamicTest`. `DynamicContainer` instances are composed of a _display name_ and a list of dynamic child nodes, enabling the creation of arbitrarily nested hierarchies of dynamic nodes. diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestFactory.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestFactory.java index 42835ebd6888..b502f382c321 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestFactory.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestFactory.java @@ -30,7 +30,9 @@ * * <p>{@code @TestFactory} methods must not be {@code private} or {@code static} * and must return a {@code Stream}, {@code Collection}, {@code Iterable}, - * {@code Iterator}, or array of {@link DynamicNode} instances. Supported + * {@code Iterator}, array of {@link DynamicNode} instances, or any type that + * provides an {@link java.util.Iterator Iterator}-returning {@code iterator()} + * method (such as, for example, a {@code kotlin.sequences.Sequence}). Supported * subclasses of {@code DynamicNode} include {@link DynamicContainer} and * {@link DynamicTest}. <em>Dynamic tests</em> will be executed lazily, * enabling dynamic and even non-deterministic generation of test cases. diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSource.java index 8c2db1a90fb1..2e1415eb8dbd 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSource.java @@ -43,11 +43,13 @@ * {@link java.util.stream.DoubleStream DoubleStream}, * {@link java.util.stream.LongStream LongStream}, or * {@link java.util.stream.IntStream IntStream}), a {@code Supplier} of an - * {@link java.util.Iterator Iterator}, an array of objects, or an array of - * primitives. Each set of "arguments" within the "stream" can be supplied as an - * instance of {@link Arguments}, an array of objects (for example, {@code Object[]}, - * {@code String[]}, etc.), or a single <em>value</em> if the parameterized test - * method accepts a single argument. + * {@link java.util.Iterator Iterator}, an array of objects or primitives, or + * any type that provides an {@link java.util.Iterator Iterator}-returning + * {@code iterator()} method (such as, for example, a + * {@code kotlin.sequences.Sequence}). Each set of "arguments" within the + * "stream" can be supplied as an instance of {@link Arguments}, an array of + * objects (for example, {@code Object[]}, {@code String[]}, etc.), or a single + * <em>value</em> if the parameterized test method accepts a single argument. * * <p>In contrast to the supported return types for {@link MethodSource @MethodSource} * factory methods, the value of a {@code @FieldSource} field cannot be an instance of diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSource.java index 2ea6da4da72f..71f02395bdc1 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSource.java @@ -42,12 +42,13 @@ * {@link java.util.stream.LongStream LongStream}, * {@link java.util.stream.IntStream IntStream}, * {@link java.util.Collection Collection}, - * {@link java.util.Iterator Iterator}, - * {@link Iterable}, an array of objects, or an array of primitives. Each set of - * "arguments" within the "stream" can be supplied as an instance of - * {@link Arguments}, an array of objects (e.g., {@code Object[]}, - * {@code String[]}, etc.), or a single <em>value</em> if the parameterized test - * method accepts a single argument. + * {@link java.util.Iterator Iterator}, an array of objects or primitives, or + * any type that provides an {@link java.util.Iterator Iterator}-returning + * {@code iterator()} method (such as, for example, a + * {@code kotlin.sequences.Sequence}). Each set of "arguments" within the + * "stream" can be supplied as an instance of {@link Arguments}, an array of + * objects (e.g., {@code Object[]}, {@code String[]}, etc.), or a single + * <em>value</em> if the parameterized test method accepts a single argument. * * <p>Please note that a one-dimensional array of objects supplied as a set of * "arguments" will be handled differently than other types of arguments. From 700f4fc1f9d7e7401cdd601483b54110f8953e47 Mon Sep 17 00:00:00 2001 From: Marc Philipp <mail@marcphilipp.de> Date: Tue, 6 May 2025 09:46:32 +0200 Subject: [PATCH 5/7] Adjust discovery issue validation message for `@TestFactory` methods --- .../engine/discovery/predicates/IsTestFactoryMethod.java | 2 +- .../discovery/predicates/IsTestFactoryMethodTests.java | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestFactoryMethod.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestFactoryMethod.java index aaf90d6db727..fcc37c85bf2f 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestFactoryMethod.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/predicates/IsTestFactoryMethod.java @@ -40,7 +40,7 @@ public class IsTestFactoryMethod extends IsTestableMethod { private static final String EXPECTED_RETURN_TYPE_MESSAGE = String.format( - "must return a single %1$s or a Stream, Collection, Iterable, Iterator, or array of %1$s", + "must return a single %1$s or a Stream, Collection, Iterable, Iterator, Iterator provider, or array of %1$s", DynamicNode.class.getName()); public IsTestFactoryMethod(DiscoveryIssueReporter issueReporter) { diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsTestFactoryMethodTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsTestFactoryMethodTests.java index f7b7534e38be..22c295d74864 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsTestFactoryMethodTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/predicates/IsTestFactoryMethodTests.java @@ -66,7 +66,8 @@ void invalidFactoryMethods(String methodName) { var issue = getOnlyElement(discoveryIssues); assertThat(issue.severity()).isEqualTo(DiscoveryIssue.Severity.WARNING); assertThat(issue.message()).isEqualTo( - "@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. " + "@TestFactory method '%s' must return a single org.junit.jupiter.api.DynamicNode or a " + + "Stream, Collection, Iterable, Iterator, Iterator provider, or array of org.junit.jupiter.api.DynamicNode. " + "It will not be executed.", method.toGenericString()); assertThat(issue.source()).contains(MethodSource.from(method)); @@ -83,7 +84,8 @@ void suspiciousFactoryMethods(String methodName) { assertThat(issue.severity()).isEqualTo(DiscoveryIssue.Severity.INFO); assertThat(issue.message()).isEqualTo( "The declared return type of @TestFactory method '%s' does not support static validation. " - + "It must return a single org.junit.jupiter.api.DynamicNode or a Stream, Collection, Iterable, Iterator, or array of org.junit.jupiter.api.DynamicNode.", + + "It must return a single org.junit.jupiter.api.DynamicNode or a " + + "Stream, Collection, Iterable, Iterator, Iterator provider, or array of org.junit.jupiter.api.DynamicNode.", method.toGenericString()); assertThat(issue.source()).contains(MethodSource.from(method)); } From 559e05550ea035f384bd745c7aee3400b4151f46 Mon Sep 17 00:00:00 2001 From: Marc Philipp <mail@marcphilipp.de> Date: Tue, 6 May 2025 09:56:57 +0200 Subject: [PATCH 6/7] Add to release notes --- .../docs/asciidoc/release-notes/release-notes-5.13.0-RC1.adoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-RC1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-RC1.adoc index 50e9e4b123cf..d52501f90ea3 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-RC1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-RC1.adoc @@ -45,7 +45,8 @@ repository on GitHub. [[release-notes-5.13.0-RC1-junit-jupiter-new-features-and-improvements]] ==== New Features and Improvements -* ❓ +* Add support for Kotlin `Sequence` to `@MethodSource`, `@FieldSource`, and + `@TestFactory`. [[release-notes-5.13.0-RC1-junit-vintage]] From 71cb1d8de0dd0e8a41e51f7c5f09acafc41006f2 Mon Sep 17 00:00:00 2001 From: Marc Philipp <mail@marcphilipp.de> Date: Tue, 6 May 2025 09:58:05 +0200 Subject: [PATCH 7/7] Update wrapping --- .../src/docs/asciidoc/user-guide/writing-tests.adoc | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc index 78c4eadc5303..b9c86dbb8a89 100644 --- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc @@ -1950,11 +1950,10 @@ translates to a `Stream` of `Arguments` (i.e., `Stream<Arguments>`); however, th concrete return type can take on many forms. In this context, a "stream" is anything that JUnit can reliably convert into a `Stream`, such as `Stream`, `DoubleStream`, `LongStream`, `IntStream`, `Collection`, `Iterator`, `Iterable`, an array of objects or -primitives, or -any type that provides an `iterator(): Iterator` method (such as, for example, a -`kotlin.sequences.Sequence`). The "arguments" within the stream can be supplied as an -instance of `Arguments`, an array of objects (e.g., `Object[]`), or a single value if the -parameterized class or test method accepts a single argument. +primitives, or any type that provides an `iterator(): Iterator` method (such as, for +example, a `kotlin.sequences.Sequence`). The "arguments" within the stream can be supplied +as an instance of `Arguments`, an array of objects (e.g., `Object[]`), or a single value +if the parameterized class or test method accepts a single argument. If the return type is `Stream` or one of the primitive streams, JUnit will properly close it by calling `BaseStream.close()`,