diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc index 20ba0cdeed290..74c97e0735685 100644 --- a/docs/src/main/asciidoc/getting-started-testing.adoc +++ b/docs/src/main/asciidoc/getting-started-testing.adoc @@ -1394,3 +1394,14 @@ For `@QuarkusIntegrationTest` tests that result in launcher the application as a This can be used by `QuarkusTestResourceLifecycleManager` that need to launch additional containers that the application will communicate with. ==== +== Troubleshooting + +=== Junit 5 parameterized tests failing on Java 16 and newer + +`@ParameterizedTest`s in JUnit 5 can fail with a `java.lang.IllegalStateException` (or +`com.thoughtworks.xstream.converters.ConversionException` in older Quarkus versions) with a message `No converter available` +for new-ish collection types like `java.util.Arrays$ArrayList` or other "JDK internal" collection types like +`java.util.ImmutableCollections$List12`. The only workaround at this point is to add necessary `--add-opens` options. + +Since `quarkus-junit5` uses [X-stream](https://github.com/x-stream/xstream) under the covers, there's no immediate +solution available, unfortunately. diff --git a/integration-tests/test-extension/tests/pom.xml b/integration-tests/test-extension/tests/pom.xml index a0c6923d8e41d..79aab17cf193d 100644 --- a/integration-tests/test-extension/tests/pom.xml +++ b/integration-tests/test-extension/tests/pom.xml @@ -118,6 +118,23 @@ uber-jar + + jdk17 + + [17,) + + + + + org.apache.maven.plugins + maven-surefire-plugin + + --add-opens java.base/java.util=ALL-UNNAMED + + + + + diff --git a/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/ParamsTest.java b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/ParamsTest.java new file mode 100644 index 0000000000000..2c1ee739766f8 --- /dev/null +++ b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/ParamsTest.java @@ -0,0 +1,126 @@ +package io.quarkus.it.extension; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import io.quarkus.test.junit.QuarkusTest; + +/** + * Exercise {@link ParameterizedTest @ParameterizedTest}s. + * + *

+ * This test will run into x-stream/xstream#253 on Java 16 and + * newer without the necessary {@code --add-opens} options. + */ +@QuarkusTest +public class ParamsTest { + @ParameterizedTest + @ValueSource(booleans = { true, false }) + public void valuesBooleans(boolean ignore) { + } + + @ParameterizedTest + @ValueSource(strings = { "true", "false" }) + public void valuesStrings(String ignore) { + } + + @ParameterizedTest + @ValueSource(classes = { String.class, TestData.class }) + public void valuesClasses(Class ignore) { + } + + @ParameterizedTest + @ValueSource(chars = { 'a', 'b', 'c' }) + public void valuesChars(char ignore) { + } + + @ParameterizedTest + @ValueSource(bytes = { (byte) 1, (byte) 2, (byte) 3 }) + public void valuesBytes(byte ignore) { + } + + @ParameterizedTest + @MethodSource("testDataStreamList") + public void methodStreamList(List ignore) { + } + + static Stream> testDataStreamList() { + return Stream.of(Arrays.asList("a"), Arrays.asList("b")); + } + + @ParameterizedTest + @MethodSource("testDataStreamListOf") + public void methodStreamListOf(List ignore) { + } + + static Stream> testDataStreamListOf() { + return Stream.of(List.of("a")); + } + + @ParameterizedTest + @MethodSource("testDataStreamSetOf") + public void methodStreamListOf(Set ignore) { + } + + static Stream> testDataStreamSetOf() { + return Stream.of(Set.of("a")); + } + + @ParameterizedTest + @MethodSource("testDataStreamArrayList") + public void methodStreamArrayList(List ignore) { + } + + static Stream> testDataStreamArrayList() { + return Stream.of(Collections.emptyList()); + } + + @ParameterizedTest + @MethodSource("testDataStream") + public void methodStream(TestData ignore) { + } + + static Stream testDataStream() { + return Stream.of(new TestData()); + } + + @ParameterizedTest + @MethodSource("testDataList") + public void methodList(TestData ignore) { + } + + static List testDataList() { + return List.of(new TestData()); + } + + @ParameterizedTest + @MethodSource("testDataStreamArguments") + public void methodList(TestData ignore, String ignored) { + } + + static Stream testDataStreamArguments() { + return Stream.of(Arguments.of(new TestData(), "foo")); + } + + @ParameterizedTest + @MethodSource("testDataListArguments") + public void methodListArguments(TestData ignore, String ignored) { + } + + static List testDataListArguments() { + return Arrays.asList(Arguments.of(new TestData(), "foo"), Arguments.of(new TestData(), "foo")); + } + + @SuppressWarnings("unused") + static class TestData { + final List foo = Arrays.asList("one", "two", "three"); + } +} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/XStreamDeepClone.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/XStreamDeepClone.java index dc945e5bf7f7c..564608dd7af6e 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/XStreamDeepClone.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/XStreamDeepClone.java @@ -3,12 +3,15 @@ import java.util.function.Supplier; import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.converters.ConversionException; /** * Super simple cloning strategy that just serializes to XML and deserializes it using xstream */ class XStreamDeepClone implements DeepClone { + static final String DEFAULT_ACTION = "Please report the issue on the Quarkus issue tracker."; + private final Supplier xStreamSupplier; XStreamDeepClone(ClassLoader classLoader) { @@ -46,12 +49,29 @@ public Object get() { private Object doClone(Object objectToClone) { XStream xStream = xStreamSupplier.get(); - final String serialized = xStream.toXML(objectToClone); + final String serialized = serialize(objectToClone, xStream); final Object result = xStream.fromXML(serialized); if (result == null) { - throw new IllegalStateException("Unable to deep clone object of type '" + objectToClone.getClass().getName() - + "'. Please report the issue on the Quarkus issue tracker."); + throw new IllegalStateException(cannotDeepCloneMessage(objectToClone, DEFAULT_ACTION)); } return result; } + + private String serialize(Object objectToClone, XStream xStream) { + try { + return xStream.toXML(objectToClone); + } catch (ConversionException e) { + String additional = e.getMessage().startsWith("No converter available") + ? "'No converter available' messages are known to occur with new-ish Java versions due " + + "to https://github.com/x-stream/xstream/issues/253, please try with JVM options proposed in " + + "the message of the nested ConversionException, " + + "if any, or report the issue on the Quarkus issue tracker.\n" + e.getMessage() + : DEFAULT_ACTION; + throw new IllegalStateException(cannotDeepCloneMessage(objectToClone, additional), e); + } + } + + private String cannotDeepCloneMessage(Object objectToClone, String additional) { + return String.format("Unable to deep clone object of type '%s'. %s", objectToClone.getClass().getName(), additional); + } }