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 &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);
@@ -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()`,