diff --git a/bom/application/pom.xml b/bom/application/pom.xml
index 7b525a5522ca0..6f28de3382cee 100644
--- a/bom/application/pom.xml
+++ b/bom/application/pom.xml
@@ -4817,11 +4817,6 @@
pom
-
- org.jboss.marshalling
- jboss-marshalling
- ${jboss-marshalling.version}
-
org.jboss.threads
jboss-threads
diff --git a/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestParameterDevModeIT.java b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestParameterDevModeIT.java
index 83355e35ca946..6219d4c8ca510 100644
--- a/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestParameterDevModeIT.java
+++ b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/it/TestParameterDevModeIT.java
@@ -7,6 +7,7 @@
import org.apache.maven.shared.invoker.MavenInvocationException;
import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
@@ -21,6 +22,7 @@
* mvn install -Dit.test=DevMojoIT#methodName
*/
@DisabledIfSystemProperty(named = "quarkus.test.native", matches = "true")
+@Disabled("Needs https://github.com/junit-team/junit5/pull/3820 and #40601")
public class TestParameterDevModeIT extends RunAndCheckMojoTestBase {
protected int getPort() {
diff --git a/test-framework/junit5/pom.xml b/test-framework/junit5/pom.xml
index 449f8fda37df5..132c4db1b6531 100644
--- a/test-framework/junit5/pom.xml
+++ b/test-framework/junit5/pom.xml
@@ -49,8 +49,10 @@
quarkus-core
- org.jboss.marshalling
- jboss-marshalling
+ com.thoughtworks.xstream
+ xstream
+
+ 1.4.20
diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java
index 15fa6c360e67b..f2707e915346b 100644
--- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java
+++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java
@@ -40,6 +40,7 @@
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
+import java.util.function.Supplier;
import java.util.regex.Pattern;
import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
@@ -51,6 +52,7 @@
import org.jboss.jandex.Type;
import org.jboss.logging.Logger;
import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
@@ -104,7 +106,7 @@
import io.quarkus.test.junit.callback.QuarkusTestContext;
import io.quarkus.test.junit.callback.QuarkusTestMethodContext;
import io.quarkus.test.junit.internal.DeepClone;
-import io.quarkus.test.junit.internal.NewSerializingDeepClone;
+import io.quarkus.test.junit.internal.SerializationWithXStreamFallbackDeepClone;
public class QuarkusTestExtension extends AbstractJvmQuarkusTestExtension
implements BeforeEachCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback, AfterEachCallback,
@@ -353,7 +355,7 @@ private void shutdownHangDetection() {
}
private void populateDeepCloneField(StartupAction startupAction) {
- deepClone = new NewSerializingDeepClone(originalCl, startupAction.getClassLoader());
+ deepClone = new SerializationWithXStreamFallbackDeepClone(startupAction.getClassLoader());
}
private void populateTestMethodInvokers(ClassLoader quarkusClassLoader) {
@@ -960,13 +962,49 @@ private Object runExtensionMethod(ReflectiveInvocationContext invocation
Parameter[] parameters = invocationContext.getExecutable().getParameters();
for (int i = 0; i < originalArguments.size(); i++) {
Object arg = originalArguments.get(i);
+ boolean cloneRequired = false;
+ Object replacement = null;
Class> argClass = parameters[i].getType();
+ if (arg != null) {
+ Class> theclass = argClass;
+ while (theclass.isArray()) {
+ theclass = theclass.getComponentType();
+ }
+ if (theclass.isPrimitive()) {
+ cloneRequired = false;
+ } else if (TestInfo.class.isAssignableFrom(theclass)) {
+ TestInfo info = (TestInfo) arg;
+ Method newTestMethod = info.getTestMethod().isPresent()
+ ? determineTCCLExtensionMethod(info.getTestMethod().get(), testClassFromTCCL)
+ : null;
+ replacement = new TestInfoImpl(info.getDisplayName(), info.getTags(),
+ Optional.of(testClassFromTCCL),
+ Optional.ofNullable(newTestMethod));
+ } else if (clonePattern.matcher(theclass.getName()).matches()) {
+ cloneRequired = true;
+ } else {
+ try {
+ cloneRequired = runningQuarkusApplication.getClassLoader()
+ .loadClass(theclass.getName()) != theclass;
+ } catch (ClassNotFoundException e) {
+ if (arg instanceof Supplier) {
+ cloneRequired = true;
+ } else {
+ throw e;
+ }
+ }
+ }
+ }
- if (testMethodInvokerToUse != null) {
+ if (replacement != null) {
+ argumentsFromTccl.add(replacement);
+ } else if (cloneRequired) {
+ argumentsFromTccl.add(deepClone.clone(arg));
+ } else if (testMethodInvokerToUse != null) {
argumentsFromTccl.add(testMethodInvokerToUse.getClass().getMethod("methodParamInstance", String.class)
.invoke(testMethodInvokerToUse, argClass.getName()));
} else {
- argumentsFromTccl.add(deepClone.clone(arg));
+ argumentsFromTccl.add(arg);
}
}
@@ -976,7 +1014,7 @@ private Object runExtensionMethod(ReflectiveInvocationContext invocation
.invoke(testMethodInvokerToUse, effectiveTestInstance, newMethod, argumentsFromTccl,
extensionContext.getRequiredTestClass().getName());
} else {
- return newMethod.invoke(effectiveTestInstance, argumentsFromTccl.toArray(Object[]::new));
+ return newMethod.invoke(effectiveTestInstance, argumentsFromTccl.toArray(new Object[0]));
}
} catch (InvocationTargetException e) {
diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/TestInfoImpl.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/TestInfoImpl.java
similarity index 95%
rename from test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/TestInfoImpl.java
rename to test-framework/junit5/src/main/java/io/quarkus/test/junit/TestInfoImpl.java
index 7cc0be697b719..498cc5ff64447 100644
--- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/TestInfoImpl.java
+++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/TestInfoImpl.java
@@ -1,4 +1,4 @@
-package io.quarkus.test.junit.internal;
+package io.quarkus.test.junit;
import java.lang.reflect.Method;
import java.util.Optional;
diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomListConverter.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomListConverter.java
new file mode 100644
index 0000000000000..ddb8642d0056c
--- /dev/null
+++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomListConverter.java
@@ -0,0 +1,63 @@
+package io.quarkus.test.junit.internal;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
+
+import com.thoughtworks.xstream.converters.collections.CollectionConverter;
+import com.thoughtworks.xstream.mapper.Mapper;
+
+/**
+ * A custom List converter that always uses ArrayList for unmarshalling.
+ * This is probably not semantically correct 100% of the time, but it's likely fine
+ * for all the cases where we are using marshalling / unmarshalling.
+ *
+ * The reason for doing this is to avoid XStream causing illegal access issues
+ * for internal JDK lists
+ */
+public class CustomListConverter extends CollectionConverter {
+
+ // if we wanted to be 100% sure, we'd list all the List.of methods, but I think it's pretty safe to say
+ // that the JDK won't add custom implementations for the other classes
+
+ private final Predicate supported = new Predicate() {
+
+ private final Set JDK_LIST_CLASS_NAMES = Set.of(
+ List.of().getClass().getName(),
+ List.of(Integer.MAX_VALUE).getClass().getName(),
+ Arrays.asList(Integer.MAX_VALUE).getClass().getName(),
+ Collections.unmodifiableList(List.of()).getClass().getName(),
+ Collections.emptyList().getClass().getName(),
+ List.of(Integer.MIN_VALUE, Integer.MAX_VALUE).subList(0, 1).getClass().getName());
+
+ @Override
+ public boolean test(String className) {
+ return JDK_LIST_CLASS_NAMES.contains(className);
+ }
+ }.or(new Predicate<>() {
+
+ private static final String GUAVA_LISTS_PACKAGE = "com.google.common.collect.Lists";
+
+ @Override
+ public boolean test(String className) {
+ return className.startsWith(GUAVA_LISTS_PACKAGE);
+ }
+ });
+
+ public CustomListConverter(Mapper mapper) {
+ super(mapper);
+ }
+
+ @Override
+ public boolean canConvert(Class type) {
+ return (type != null) && supported.test(type.getName());
+ }
+
+ @Override
+ protected Object createCollection(Class type) {
+ return new ArrayList<>();
+ }
+}
diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomMapConverter.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomMapConverter.java
new file mode 100644
index 0000000000000..fe93cb8594587
--- /dev/null
+++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomMapConverter.java
@@ -0,0 +1,41 @@
+package io.quarkus.test.junit.internal;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import com.thoughtworks.xstream.converters.collections.MapConverter;
+import com.thoughtworks.xstream.mapper.Mapper;
+
+/**
+ * A custom Map converter that always uses HashMap for unmarshalling.
+ * This is probably not semantically correct 100% of the time, but it's likely fine
+ * for all the cases where we are using marshalling / unmarshalling.
+ *
+ * The reason for doing this is to avoid XStream causing illegal access issues
+ * for internal JDK maps
+ */
+public class CustomMapConverter extends MapConverter {
+
+ // if we wanted to be 100% sure, we'd list all the Set.of methods, but I think it's pretty safe to say
+ // that the JDK won't add custom implementations for the other classes
+ private final Set SUPPORTED_CLASS_NAMES = Set.of(
+ Map.of().getClass().getName(),
+ Map.of(Integer.MAX_VALUE, Integer.MAX_VALUE).getClass().getName(),
+ Collections.emptyMap().getClass().getName());
+
+ public CustomMapConverter(Mapper mapper) {
+ super(mapper);
+ }
+
+ @Override
+ public boolean canConvert(Class type) {
+ return (type != null) && SUPPORTED_CLASS_NAMES.contains(type.getName());
+ }
+
+ @Override
+ protected Object createCollection(Class type) {
+ return new HashMap<>();
+ }
+}
diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomMapEntryConverter.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomMapEntryConverter.java
new file mode 100644
index 0000000000000..f20a7fe3e3f36
--- /dev/null
+++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomMapEntryConverter.java
@@ -0,0 +1,55 @@
+package io.quarkus.test.junit.internal;
+
+import java.util.AbstractMap;
+import java.util.Map;
+import java.util.Set;
+
+import com.thoughtworks.xstream.converters.MarshallingContext;
+import com.thoughtworks.xstream.converters.UnmarshallingContext;
+import com.thoughtworks.xstream.converters.collections.MapConverter;
+import com.thoughtworks.xstream.io.HierarchicalStreamReader;
+import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+import com.thoughtworks.xstream.mapper.Mapper;
+
+/**
+ * A custom Map.Entry converter that always uses AbstractMap.SimpleEntry for unmarshalling.
+ * This is probably not semantically correct 100% of the time, but it's likely fine
+ * for all the cases where we are using marshalling / unmarshalling.
+ *
+ * The reason for doing this is to avoid XStream causing illegal access issues
+ * for internal JDK types
+ */
+@SuppressWarnings({ "rawtypes", "unchecked" })
+public class CustomMapEntryConverter extends MapConverter {
+
+ private final Set SUPPORTED_CLASS_NAMES = Set
+ .of(Map.entry(Integer.MAX_VALUE, Integer.MAX_VALUE).getClass().getName());
+
+ public CustomMapEntryConverter(Mapper mapper) {
+ super(mapper);
+ }
+
+ @Override
+ public boolean canConvert(Class type) {
+ return (type != null) && SUPPORTED_CLASS_NAMES.contains(type.getName());
+ }
+
+ @Override
+ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
+ var entryName = mapper().serializedClass(Map.Entry.class);
+ var entry = (Map.Entry) source;
+ writer.startNode(entryName);
+ writeCompleteItem(entry.getKey(), context, writer);
+ writeCompleteItem(entry.getValue(), context, writer);
+ writer.endNode();
+ }
+
+ @Override
+ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
+ reader.moveDown();
+ var key = readCompleteItem(reader, context, null);
+ var value = readCompleteItem(reader, context, null);
+ reader.moveUp();
+ return new AbstractMap.SimpleEntry(key, value);
+ }
+}
diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomSetConverter.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomSetConverter.java
new file mode 100644
index 0000000000000..88d434cfaf34a
--- /dev/null
+++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/CustomSetConverter.java
@@ -0,0 +1,40 @@
+package io.quarkus.test.junit.internal;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import com.thoughtworks.xstream.converters.collections.CollectionConverter;
+import com.thoughtworks.xstream.mapper.Mapper;
+
+/**
+ * A custom Set converter that always uses HashSet for unmarshalling.
+ * This is probably not semantically correct 100% of the time, but it's likely fine
+ * for all the cases where we are using marshalling / unmarshalling.
+ *
+ * The reason for doing this is to avoid XStream causing illegal access issues
+ * for internal JDK sets
+ */
+public class CustomSetConverter extends CollectionConverter {
+
+ // if we wanted to be 100% sure, we'd list all the Set.of methods, but I think it's pretty safe to say
+ // that the JDK won't add custom implementations for the other classes
+ private final Set SUPPORTED_CLASS_NAMES = Set.of(
+ Set.of().getClass().getName(),
+ Set.of(Integer.MAX_VALUE).getClass().getName(),
+ Collections.emptySet().getClass().getName());
+
+ public CustomSetConverter(Mapper mapper) {
+ super(mapper);
+ }
+
+ @Override
+ public boolean canConvert(Class type) {
+ return (type != null) && SUPPORTED_CLASS_NAMES.contains(type.getName());
+ }
+
+ @Override
+ protected Object createCollection(Class type) {
+ return new HashSet<>();
+ }
+}
diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/NewSerializingDeepClone.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/NewSerializingDeepClone.java
deleted file mode 100644
index 682a196e00c71..0000000000000
--- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/NewSerializingDeepClone.java
+++ /dev/null
@@ -1,113 +0,0 @@
-package io.quarkus.test.junit.internal;
-
-import java.io.IOException;
-import java.io.Serializable;
-import java.io.UncheckedIOException;
-import java.lang.reflect.Method;
-import java.util.Set;
-import java.util.function.Supplier;
-
-import org.jboss.marshalling.cloner.ClassCloner;
-import org.jboss.marshalling.cloner.ClonerConfiguration;
-import org.jboss.marshalling.cloner.ObjectCloner;
-import org.jboss.marshalling.cloner.ObjectCloners;
-import org.junit.jupiter.api.TestInfo;
-
-/**
- * A deep-clone implementation using JBoss Marshalling's fast object cloner.
- */
-public final class NewSerializingDeepClone implements DeepClone {
- private final ObjectCloner cloner;
-
- public NewSerializingDeepClone(final ClassLoader sourceLoader, final ClassLoader targetLoader) {
- ClonerConfiguration cc = new ClonerConfiguration();
- cc.setSerializabilityChecker(clazz -> clazz != Object.class);
- cc.setClassCloner(new ClassCloner() {
- public Class> clone(final Class> original) {
- if (isUncloneable(original)) {
- return original;
- }
- try {
- return targetLoader.loadClass(original.getName());
- } catch (ClassNotFoundException ignored) {
- return original;
- }
- }
-
- public Class> cloneProxy(final Class> proxyClass) {
- // not really supported
- return proxyClass;
- }
- });
- cc.setCloneTable(
- (original, objectCloner, classCloner) -> {
- if (EXTRA_IDENTITY_CLASSES.contains(original.getClass())) {
- // avoid copying things that do not need to be copied
- return original;
- } else if (isUncloneable(original.getClass())) {
- if (original instanceof Supplier> s) {
- // sneaky
- return (Supplier>) () -> clone(s.get());
- } else {
- return original;
- }
- } else if (original instanceof TestInfo info) {
- // copy the test info correctly
- return new TestInfoImpl(info.getDisplayName(), info.getTags(),
- info.getTestClass().map(this::cloneClass),
- info.getTestMethod().map(this::cloneMethod));
- } else if (original == sourceLoader) {
- return targetLoader;
- }
- // let the default cloner handle it
- return null;
- });
- cloner = ObjectCloners.getSerializingObjectClonerFactory().createCloner(cc);
- }
-
- private static boolean isUncloneable(Class> clazz) {
- return clazz.isHidden() && !Serializable.class.isAssignableFrom(clazz);
- }
-
- private Class> cloneClass(Class> clazz) {
- try {
- return (Class>) cloner.clone(clazz);
- } catch (IOException | ClassNotFoundException e) {
- return null;
- }
- }
-
- private Method cloneMethod(Method method) {
- try {
- Class> declaring = (Class>) cloner.clone(method.getDeclaringClass());
- Class>[] argTypes = (Class>[]) cloner.clone(method.getParameterTypes());
- return declaring.getDeclaredMethod(method.getName(), argTypes);
- } catch (Exception e) {
- return null;
- }
- }
-
- public Object clone(final Object objectToClone) {
- try {
- return cloner.clone(objectToClone);
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- } catch (ClassNotFoundException e) {
- throw new IllegalStateException(e);
- }
- }
-
- /**
- * Classes which do not need to be cloned.
- */
- private static final Set> EXTRA_IDENTITY_CLASSES = Set.of(
- Object.class,
- byte[].class,
- short[].class,
- int[].class,
- long[].class,
- char[].class,
- boolean[].class,
- float[].class,
- double[].class);
-}
diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/SerializationDeepClone.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/SerializationDeepClone.java
new file mode 100644
index 0000000000000..3da2c0c16e372
--- /dev/null
+++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/SerializationDeepClone.java
@@ -0,0 +1,46 @@
+package io.quarkus.test.junit.internal;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.ObjectStreamClass;
+
+/**
+ * Cloning strategy that just serializes and deserializes using plain old java serialization.
+ */
+class SerializationDeepClone implements DeepClone {
+
+ private final ClassLoader classLoader;
+
+ SerializationDeepClone(ClassLoader classLoader) {
+ this.classLoader = classLoader;
+ }
+
+ @Override
+ public Object clone(Object objectToClone) {
+ ByteArrayOutputStream byteOut = new ByteArrayOutputStream(512);
+ try (ObjectOutputStream objOut = new ObjectOutputStream(byteOut)) {
+ objOut.writeObject(objectToClone);
+ try (ObjectInputStream objIn = new ClassLoaderAwareObjectInputStream(byteOut)) {
+ return objIn.readObject();
+ }
+ } catch (IOException | ClassNotFoundException e) {
+ throw new IllegalStateException("Unable to deep clone object of type '" + objectToClone.getClass().getName()
+ + "'. Please report the issue on the Quarkus issue tracker.", e);
+ }
+ }
+
+ private class ClassLoaderAwareObjectInputStream extends ObjectInputStream {
+
+ public ClassLoaderAwareObjectInputStream(ByteArrayOutputStream byteOut) throws IOException {
+ super(new ByteArrayInputStream(byteOut.toByteArray()));
+ }
+
+ @Override
+ protected Class> resolveClass(ObjectStreamClass desc) throws ClassNotFoundException {
+ return Class.forName(desc.getName(), true, classLoader);
+ }
+ }
+}
diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/SerializationWithXStreamFallbackDeepClone.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/SerializationWithXStreamFallbackDeepClone.java
new file mode 100644
index 0000000000000..36da89a82e804
--- /dev/null
+++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/SerializationWithXStreamFallbackDeepClone.java
@@ -0,0 +1,35 @@
+package io.quarkus.test.junit.internal;
+
+import java.io.Serializable;
+import java.util.Optional;
+
+import org.jboss.logging.Logger;
+
+/**
+ * Cloning strategy delegating to {@link SerializationDeepClone}, falling back to {@link XStreamDeepClone} in case of error.
+ */
+public class SerializationWithXStreamFallbackDeepClone implements DeepClone {
+
+ private static final Logger LOG = Logger.getLogger(SerializationWithXStreamFallbackDeepClone.class);
+
+ private final SerializationDeepClone serializationDeepClone;
+ private final XStreamDeepClone xStreamDeepClone;
+
+ public SerializationWithXStreamFallbackDeepClone(ClassLoader classLoader) {
+ this.serializationDeepClone = new SerializationDeepClone(classLoader);
+ this.xStreamDeepClone = new XStreamDeepClone(classLoader);
+ }
+
+ @Override
+ public Object clone(Object objectToClone) {
+ if (objectToClone instanceof Serializable) {
+ try {
+ return serializationDeepClone.clone(objectToClone);
+ } catch (RuntimeException re) {
+ LOG.debugf("SerializationDeepClone failed (will fall back to XStream): %s",
+ Optional.ofNullable(re.getCause()).orElse(re));
+ }
+ }
+ return xStreamDeepClone.clone(objectToClone);
+ }
+}
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
new file mode 100644
index 0000000000000..9951f96734d44
--- /dev/null
+++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/internal/XStreamDeepClone.java
@@ -0,0 +1,61 @@
+package io.quarkus.test.junit.internal;
+
+import java.util.function.Supplier;
+
+import com.thoughtworks.xstream.XStream;
+
+/**
+ * Super simple cloning strategy that just serializes to XML and deserializes it using xstream
+ */
+class XStreamDeepClone implements DeepClone {
+
+ private final Supplier xStreamSupplier;
+
+ XStreamDeepClone(ClassLoader classLoader) {
+ // avoid doing any work eagerly since the cloner is rarely used
+ xStreamSupplier = () -> {
+ XStream result = new XStream();
+ result.allowTypesByRegExp(new String[] { ".*" });
+ result.setClassLoader(classLoader);
+ result.registerConverter(new CustomListConverter(result.getMapper()));
+ result.registerConverter(new CustomSetConverter(result.getMapper()));
+ result.registerConverter(new CustomMapConverter(result.getMapper()));
+ result.registerConverter(new CustomMapEntryConverter(result.getMapper()));
+
+ return result;
+ };
+ }
+
+ @Override
+ public Object clone(Object objectToClone) {
+ if (objectToClone == null) {
+ return null;
+ }
+
+ if (objectToClone instanceof Supplier) {
+ return handleSupplier((Supplier>) objectToClone);
+ }
+
+ return doClone(objectToClone);
+ }
+
+ private Supplier