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..799f947a62b7 --- /dev/null +++ b/junit-jupiter-engine/src/test/kotlin/org/junit/jupiter/api/KotlinDynamicTests.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ +package org.junit.jupiter.api + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.DynamicTest.dynamicTest +import java.math.BigDecimal +import java.math.BigDecimal.ONE +import java.math.MathContext +import java.math.BigInteger as BigInt +import java.math.RoundingMode as Rounding + +/** + * Unit tests for JUnit Jupiter [TestFactory] use in kotlin classes. + * + * @since 5.10 + */ +class KotlinDynamicTests { + + @Nested + inner class SequenceReturningTestFactoryTests { + + @TestFactory + fun `Dynamic tests returned as Kotlin sequence`() = generateSequence(0) { it + 2 } + .map { dynamicTest("$it should be even") { assertTrue(it % 2 == 0) } } + .take(10) + + @TestFactory + fun `Is anagram tests`(): Sequence { + infix fun CharSequence.isAngramOf(other: CharSequence) = groupBy { it } == other.groupBy { it } + + infix fun CharSequence.`should be an anagram of`(other: CharSequence) = + dynamicTest("'$this' should be an anagram of '$other'") { assertTrue(this isAngramOf other) } + + infix fun CharSequence.`should not be an anagram of`(other: CharSequence) = + dynamicTest("'$this' should not be an anagram of '$other'") { assertFalse(this isAngramOf other) } + + return sequenceOf( + "a gentleman" `should be an anagram of` "elegant man", + "laptop machines" `should be an anagram of` "apple macintosh", + "salvador dali" `should be an anagram of` "avida dollars", + "a gentleman" `should not be an anagram of` "spider man", + "laptop computers" `should not be an anagram of` "apple macintosh", + "salvador dali" `should not be an anagram of` "picasso" + ) + } + + @TestFactory + fun `Consecutive fibonacci nr ratios, should converge to golden ratio as n increases`(): Sequence { + 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..19e4a8da2a75 --- /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.MethodSource +import java.time.Month +import java.util.* + +/** + * 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( + arrayOf(1, Month.JANUARY), + arrayOf(3, Month.MARCH), + arrayOf(8, Month.AUGUST), + arrayOf(5, Month.MAY), + arrayOf(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 3d08f95a8d35..c4b790e2f20b 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; @@ -99,7 +101,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);
@@ -138,8 +140,12 @@ public static boolean isConvertibleToStream(Class type) {
 				|| LongStream.class.isAssignableFrom(type)//
 				|| Iterable.class.isAssignableFrom(type)//
 				|| Iterator.class.isAssignableFrom(type)//
+				|| Spliterator.class.isAssignableFrom(type)//
 				|| Object[].class.isAssignableFrom(type)//
-				|| (type.isArray() && type.getComponentType().isPrimitive()));
+				|| (type.isArray() && type.getComponentType().isPrimitive())//
+				|| Arrays.stream(type.getMethods())//
+						.map(Method::getReturnType)//
+						.anyMatch(returnType -> returnType == Iterator.class || returnType == Spliterator.class));
 	}
 
 	/**
@@ -153,8 +159,10 @@ public static boolean isConvertibleToStream(Class type) {
 	 * 
  • {@link Collection}
  • *
  • {@link Iterable}
  • *
  • {@link Iterator}
  • + *
  • {@link Spliterator}
  • *
  • {@link Object} array
  • *
  • primitive array
  • + *
  • An object that contains an iterator or spliterator returning method
  • * * * @param object the object to convert into a stream; never {@code null} @@ -186,6 +194,9 @@ public static Stream toStream(Object object) { if (object instanceof Iterator) { return stream(spliteratorUnknownSize((Iterator) object, ORDERED), false); } + if (object instanceof Spliterator) { + return stream((Spliterator) object, false); + } if (object instanceof Object[]) { return Arrays.stream((Object[]) object); } @@ -201,8 +212,7 @@ public static Stream toStream(Object object) { if (object.getClass().isArray() && object.getClass().getComponentType().isPrimitive()) { return IntStream.range(0, Array.getLength(object)).mapToObj(i -> Array.get(object, i)); } - throw new PreconditionViolationException( - "Cannot convert instance of " + object.getClass().getName() + " into a Stream: " + object); + return StreamUtils.tryConvertToStreamByReflection(object); } /** diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/StreamUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/StreamUtils.java new file mode 100644 index 000000000000..1c629d251f71 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/StreamUtils.java @@ -0,0 +1,109 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.commons.util; + +import static java.util.Spliterator.ORDERED; +import static java.util.Spliterators.spliteratorUnknownSize; +import static java.util.stream.StreamSupport.stream; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Optional; +import java.util.Spliterator; +import java.util.stream.Stream; + +import org.junit.platform.commons.JUnitException; +import org.junit.platform.commons.PreconditionViolationException; +import org.junit.platform.commons.function.Try; + +/** + * Collection of utilities for working with {@link Stream Streams}. + * + * @since 5.10 + */ +final class StreamUtils { + + private StreamUtils() { + } + + static Stream tryConvertToStreamByReflection(Object object) { + Preconditions.notNull(object, "Object must not be null"); + Class theClass = object.getClass(); + try { + String name = "iterator"; + Method method = theClass.getMethod(name); + if (method.getReturnType() == Iterator.class) { + return stream(() -> tryIteratorToSpliterator(object, method), ORDERED, false); + } + else { + throw new IllegalStateException( + "Method with name 'iterator' does not return " + Iterator.class.getName()); + } + } + catch (NoSuchMethodException | IllegalStateException e) { + return tryConvertToStreamBySpliterator(object, e); + } + } + + private static Stream tryConvertToStreamBySpliterator(Object object, Exception e) { + try { + String name = "spliterator"; + Method method = object.getClass().getMethod(name); + if (method.getReturnType() == Spliterator.class) { + return stream(() -> tryInvokeSpliterator(object, method), ORDERED, false); + } + else { + throw new IllegalStateException( + "Method with name '" + name + "' does not return " + Spliterator.class.getName()); + } + } + catch (NoSuchMethodException | IllegalStateException ex) { + ex.addSuppressed(e); + return tryConvertByIteratorSpliteratorReturnType(object, ex); + } + } + + private static Stream tryConvertByIteratorSpliteratorReturnType(Object object, Exception ex) { + return streamFromSpliteratorSupplier(object)// + .orElseGet(() -> streamFromIteratorSupplier(object)// + .orElseThrow(() -> // + new PreconditionViolationException(// + "Cannot convert instance of " + object.getClass().getName() + " into a Stream: " + object, + ex))); + } + + private static Optional> streamFromSpliteratorSupplier(Object object) { + return Arrays.stream(object.getClass().getMethods())// + .filter(m -> m.getReturnType() == Spliterator.class)// + .findFirst()// + .map(m -> stream(() -> tryInvokeSpliterator(object, m), ORDERED, false));// + } + + private static Optional> streamFromIteratorSupplier(Object object) { + return Arrays.stream(object.getClass().getMethods())// + .filter(m -> m.getReturnType() == Iterator.class)// + .findFirst()// + .map(m -> stream(() -> tryIteratorToSpliterator(object, m), ORDERED, false));// + } + + private static Spliterator tryInvokeSpliterator(Object object, Method method) { + return Try.call(() -> (Spliterator) method.invoke(object))// + .getOrThrow(e -> new JUnitException("Cannot invoke method " + method.getName() + " onto " + object, e));// + } + + private static Spliterator tryIteratorToSpliterator(Object object, Method method) { + return Try.call(() -> method.invoke(object))// + .andThen(m -> Try.call(() -> spliteratorUnknownSize((Iterator) m, 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 63563e8071ce..642159695910 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; @@ -60,8 +63,9 @@ void getOnlyElementWithNullCollection() { @Test void getOnlyElementWithEmptyCollection() { + Set emptySet = Set.of(); var exception = assertThrows(PreconditionViolationException.class, - () -> CollectionUtils.getOnlyElement(Set.of())); + () -> CollectionUtils.getOnlyElement(emptySet)); assertEquals("collection must contain exactly one element: []", exception.getMessage()); } @@ -74,8 +78,9 @@ void getOnlyElementWithSingleElementCollection() { @Test void getOnlyElementWithMultiElementCollection() { + List strings = List.of("foo", "bar"); var exception = assertThrows(PreconditionViolationException.class, - () -> CollectionUtils.getOnlyElement(List.of("foo", "bar"))); + () -> CollectionUtils.getOnlyElement(strings)); assertEquals("collection must contain exactly one element: [foo, bar]", exception.getMessage()); } @@ -94,6 +99,9 @@ void toUnmodifiableListThrowsOnMutation() { Collection.class, // Iterable.class, // Iterator.class, // + Spliterator.class, // + MySpliteratorProvider.class, // + MyIteratorProvider.class, // Object[].class, // String[].class, // int[].class, // @@ -118,7 +126,9 @@ static Stream objectsConvertibleToStreams() { LongStream.of(100000000), // Set.of(1, 2, 3), // Arguments.of((Object) new Object[] { 9, 8, 7 }), // - new int[] { 5, 10, 15 }// + new int[] { 5, 10, 15 }, // + MySpliteratorProvider.of(new String[] { "mouse", "bear" }), // + MyIteratorProvider.of(new Integer[] { 1, 2, 3, 4, 5 })// ); } @@ -196,7 +206,7 @@ void toStreamWithLongStream() { } @Test - @SuppressWarnings({ "unchecked", "serial" }) + @SuppressWarnings({ "unchecked" }) void toStreamWithCollection() { var collectionStreamClosed = new AtomicBoolean(false); Collection input = new ArrayList<>() { @@ -241,6 +251,36 @@ void toStreamWithIterator() { assertThat(result).containsExactly("foo", "bar"); } + @Test + @SuppressWarnings("unchecked") + void toStreamWithSpliterator() { + final var input = List.of("foo", "bar").spliterator(); + + final var result = (Stream) CollectionUtils.toStream(input); + + assertThat(result).containsExactly("foo", "bar"); + } + + @Test + @SuppressWarnings("unchecked") + void toStreamWithIteratorProvider() { + final var input = MyIteratorProvider.of(new String[] { "foo", "bar" }); + + final var result = (Stream) CollectionUtils.toStream(input); + + assertThat(result).containsExactly("foo", "bar"); + } + + @Test + @SuppressWarnings("unchecked") + void toStreamWithSpliteratorProvider() { + final var input = MySpliteratorProvider.of(new String[] { "foo", "bar" }); + + var result = (Stream) CollectionUtils.toStream(input); + + assertThat(result).containsExactly("foo", "bar"); + } + @Test @SuppressWarnings("unchecked") void toStreamWithArray() { @@ -304,4 +344,26 @@ public Object convert(Object source, ParameterContext context) throws ArgumentCo return source == null ? List.of() : List.of(((String) source).split(",")); } } + + @FunctionalInterface + private interface MySpliteratorProvider { + + @SuppressWarnings("unused") + Spliterator thisReturnsASpliterator(); + + static MySpliteratorProvider of(T[] elements) { + return () -> Arrays.spliterator(elements); + } + } + + @FunctionalInterface + private interface MyIteratorProvider { + + @SuppressWarnings("unused") + Iterator thisReturnsAnIterator(); + + static MyIteratorProvider of(T[] elements) { + return () -> Spliterators.iterator(Arrays.spliterator(elements)); + } + } } diff --git a/platform-tests/src/test/java/org/junit/platform/commons/util/StreamUtilsTest.java b/platform-tests/src/test/java/org/junit/platform/commons/util/StreamUtilsTest.java new file mode 100644 index 000000000000..6fda1a6e28d9 --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/commons/util/StreamUtilsTest.java @@ -0,0 +1,121 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.commons.util; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Iterator; +import java.util.List; +import java.util.Spliterator; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.platform.commons.PreconditionViolationException; + +class StreamUtilsTest { + + @Test + void throwWhenIteratorNamedMethodDoesNotReturnAnIterator() { + var o = IteratorProviderNotUsable.of("Test"); + var e = assertThrows(PreconditionViolationException.class, () -> StreamUtils.tryConvertToStreamByReflection(o)); + + e.printStackTrace(); + assertEquals(1, e.getCause().getSuppressed().length); + } + + @SuppressWarnings("unchecked") + @Test + void usesSpliteratorToConvertToStream() { + var object = SpliteratorProvider.of(List.of("this", "is", "a", "test")); + + var result = (Stream) StreamUtils.tryConvertToStreamByReflection(object); + + assertThat(result).containsExactly("this", "is", "a", "test"); + } + + @SuppressWarnings("unchecked") + @Test + void usesIteratorExposingMethodToConvertToStream() { + var object = IteratorExposing.of(List.of("this", "is", "a", "test")); + + var result = (Stream) StreamUtils.tryConvertToStreamByReflection(object); + + assertThat(result).containsExactly("this", "is", "a", "test"); + } + + /** + * An interface that has an iterator method but does not return java.util/Iterator as a return type + */ + @FunctionalInterface + private interface IteratorProviderNotUsable { + @SuppressWarnings("unused") + Object iterator(); + + static IteratorProviderNotUsable of(Object o) { + return () -> o; + } + } + + /** + * An object that exposes: + *
      + *
    1. a method with name 'iterator' that does not return an iterator
    2. + *
    3. a method with name 'spliterator' that returns a java.util.Spliterator
    4. + *
    + * @param The type of the spliterator + */ + @FunctionalInterface + private interface SpliteratorProvider { + + @SuppressWarnings("unused") + default Object iterator() { + return null; + } + + @SuppressWarnings("unused") + Spliterator spliterator(); + + static SpliteratorProvider of(Iterable iterable) { + return iterable::spliterator; + } + + } + + /** + * An object that exposes: + *
      + *
    1. a method with name 'iterator' that does not return an iterator
    2. + *
    3. a method with name 'spliterator' that does not return a java.util.Spliterator
    4. + *
    5. a method with other name than 'iterator' returning an iterator
    6. + *
    + */ + private interface IteratorExposing { + + @SuppressWarnings("unused") + default Object iterator() { + return null; + } + + @SuppressWarnings("unused") + default Object spliterator() { + return null; + } + + @SuppressWarnings("unused") + Iterator returnsAnIterator(); + + static IteratorExposing of(Iterable iterable) { + return iterable::iterator; + } + } +}