diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessor.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessor.java index 40bf7213e1c7..54a5fe6fc154 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessor.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessor.java @@ -21,6 +21,7 @@ import org.apiguardian.api.API; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.converter.DefaultArgumentConverter; +import org.junit.platform.commons.support.conversion.TypeDescriptor; import org.junit.platform.commons.util.ClassUtils; import org.junit.platform.commons.util.Preconditions; @@ -46,7 +47,7 @@ public static DefaultArgumentsAccessor create(ExtensionContext context, int invo Preconditions.notNull(classLoader, "ClassLoader must not be null"); BiFunction, Object> converter = (source, targetType) -> new DefaultArgumentConverter(context) // - .convert(source, targetType, classLoader); + .convert(source, TypeDescriptor.forType(targetType), classLoader); return new DefaultArgumentsAccessor(converter, invocationIndex, arguments); } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java index eea0e734508a..abc7f44a615d 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java @@ -29,6 +29,7 @@ import org.junit.jupiter.params.support.FieldContext; import org.junit.platform.commons.support.conversion.ConversionException; import org.junit.platform.commons.support.conversion.ConversionSupport; +import org.junit.platform.commons.support.conversion.TypeDescriptor; import org.junit.platform.commons.util.ReflectionUtils; /** @@ -42,7 +43,7 @@ * {@link File}, {@link BigDecimal}, {@link BigInteger}, {@link Currency}, * {@link Locale}, {@link URI}, {@link URL}, {@link UUID}, etc. * - *

If the source and target types are identical the source object will not + *

If the source and target types are identical, the source object will not * be modified. * * @since 5.0 @@ -81,47 +82,41 @@ public DefaultArgumentConverter(ExtensionContext context) { @Override public final Object convert(Object source, ParameterContext context) { - Class targetType = context.getParameter().getType(); ClassLoader classLoader = getClassLoader(context.getDeclaringExecutable().getDeclaringClass()); - return convert(source, targetType, classLoader); + return convert(source, TypeDescriptor.forParameter(context.getParameter()), classLoader); } @Override public final Object convert(Object source, FieldContext context) throws ArgumentConversionException { - Class targetType = context.getField().getType(); ClassLoader classLoader = getClassLoader(context.getField().getDeclaringClass()); - return convert(source, targetType, classLoader); + return convert(source, TypeDescriptor.forField(context.getField()), classLoader); } - public final Object convert(Object source, Class targetType, ClassLoader classLoader) { + public final Object convert(Object source, TypeDescriptor targetType, ClassLoader classLoader) { if (source == null) { if (targetType.isPrimitive()) { throw new ArgumentConversionException( - "Cannot convert null to primitive value of type " + targetType.getTypeName()); + "Cannot convert null to primitive value of type " + targetType.getType().getTypeName()); } return null; } - if (ReflectionUtils.isAssignableTo(source, targetType)) { + if (ReflectionUtils.isAssignableTo(source, targetType.getType())) { return source; } - if (source instanceof String) { - if (targetType == Locale.class && getLocaleConversionFormat() == LocaleConversionFormat.BCP_47) { - return Locale.forLanguageTag((String) source); - } - - try { - return convert((String) source, targetType, classLoader); - } - catch (ConversionException ex) { - throw new ArgumentConversionException(ex.getMessage(), ex); - } + if (source instanceof String // + && targetType.getType() == Locale.class // + && getLocaleConversionFormat() == LocaleConversionFormat.BCP_47) { + return Locale.forLanguageTag((String) source); } - throw new ArgumentConversionException( - String.format("No built-in converter for source type %s and target type %s", - source.getClass().getTypeName(), targetType.getTypeName())); + try { + return delegateConversion(source, targetType, classLoader); + } + catch (ConversionException ex) { + throw new ArgumentConversionException(ex.getMessage(), ex); + } } private LocaleConversionFormat getLocaleConversionFormat() { @@ -129,7 +124,7 @@ private LocaleConversionFormat getLocaleConversionFormat() { .orElse(LocaleConversionFormat.BCP_47); } - Object convert(String source, Class targetType, ClassLoader classLoader) { + Object delegateConversion(Object source, TypeDescriptor targetType, ClassLoader classLoader) { return ConversionSupport.convert(source, targetType, classLoader); } diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionSupport.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionSupport.java index fa95826f5ddd..b26c76a1fcd7 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionSupport.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionSupport.java @@ -10,13 +10,12 @@ package org.junit.platform.commons.support.conversion; -import static java.util.Arrays.asList; -import static java.util.Collections.unmodifiableList; +import static org.apiguardian.api.API.Status.DEPRECATED; import static org.apiguardian.api.API.Status.EXPERIMENTAL; -import static org.junit.platform.commons.util.ReflectionUtils.getWrapperType; -import java.util.List; -import java.util.Optional; +import java.util.ServiceLoader; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; import org.apiguardian.api.API; import org.junit.platform.commons.util.ClassLoaderUtils; @@ -30,17 +29,6 @@ @API(status = EXPERIMENTAL, since = "1.11") public final class ConversionSupport { - private static final List stringToObjectConverters = unmodifiableList(asList( // - new StringToBooleanConverter(), // - new StringToCharacterConverter(), // - new StringToNumberConverter(), // - new StringToClassConverter(), // - new StringToEnumConverter(), // - new StringToJavaTimeConverter(), // - new StringToCommonJavaTypesConverter(), // - new FallbackStringToObjectConverter() // - )); - private ConversionSupport() { /* no-op */ } @@ -49,43 +37,6 @@ private ConversionSupport() { * Convert the supplied source {@code String} into an instance of the specified * target type. * - *

If the target type is {@code String}, the source {@code String} will not - * be modified. - * - *

Some forms of conversion require a {@link ClassLoader}. If none is - * provided, the {@linkplain ClassLoaderUtils#getDefaultClassLoader() default - * ClassLoader} will be used. - * - *

This method is able to convert strings into primitive types and their - * corresponding wrapper types ({@link Boolean}, {@link Character}, {@link Byte}, - * {@link Short}, {@link Integer}, {@link Long}, {@link Float}, and - * {@link Double}), enum constants, date and time types from the - * {@code java.time} package, as well as common Java types such as {@link Class}, - * {@link java.io.File}, {@link java.nio.file.Path}, {@link java.nio.charset.Charset}, - * {@link java.math.BigDecimal}, {@link java.math.BigInteger}, - * {@link java.util.Currency}, {@link java.util.Locale}, {@link java.util.UUID}, - * {@link java.net.URI}, and {@link java.net.URL}. - * - *

If the target type is not covered by any of the above, a convention-based - * conversion strategy will be used to convert the source {@code String} into the - * given target type by invoking a static factory method or factory constructor - * defined in the target type. The search algorithm used in this strategy is - * outlined below. - * - *

Search Algorithm

- * - *
    - *
  1. Search for a single, non-private static factory method in the target - * type that converts from a String to the target type. Use the factory method - * if present.
  2. - *
  3. Search for a single, non-private constructor in the target type that - * accepts a String. Use the constructor if present.
  4. - *
- * - *

If multiple suitable factory methods are discovered they will be ignored. - * If neither a single factory method nor a single constructor is found, the - * convention-based conversion strategy will not apply. - * * @param source the source {@code String} to convert; may be {@code null} * but only if the target type is a reference type * @param targetType the target type the source should be converted into; @@ -97,48 +48,50 @@ private ConversionSupport() { * type is a reference type * * @since 1.11 + * @see DefaultConverter + * @deprecated Use {@link #convert(Object, TypeDescriptor, ClassLoader)} instead. */ - @SuppressWarnings("unchecked") + @Deprecated + @API(status = DEPRECATED, since = "5.13") public static T convert(String source, Class targetType, ClassLoader classLoader) { - if (source == null) { - if (targetType.isPrimitive()) { - throw new ConversionException( - "Cannot convert null to primitive value of type " + targetType.getTypeName()); - } - return null; - } + return convert(source, TypeDescriptor.forType(targetType), getClassLoader(classLoader)); + } - if (String.class.equals(targetType)) { - return (T) source; - } + /** + * Convert the supplied source object into an instance of the specified + * target type. + * + * @param source the source object to convert; may be {@code null} + * but only if the target type is a reference type + * @param targetType the target type the source should be converted into; + * never {@code null} + * @param classLoader the {@code ClassLoader} to use; may be {@code null} to + * use the default {@code ClassLoader} + * @param the type of the target + * @return the converted object; may be {@code null} but only if the target + * type is a reference type + * + * @since 1.13 + */ + @API(status = EXPERIMENTAL, since = "1.13") + @SuppressWarnings("unchecked") + public static T convert(Object source, TypeDescriptor targetType, ClassLoader classLoader) { + ClassLoader classLoaderToUse = getClassLoader(classLoader); + ServiceLoader serviceLoader = ServiceLoader.load(Converter.class, classLoaderToUse); - Class targetTypeToUse = toWrapperType(targetType); - Optional converter = stringToObjectConverters.stream().filter( - candidate -> candidate.canConvertTo(targetTypeToUse)).findFirst(); - if (converter.isPresent()) { - try { - ClassLoader classLoaderToUse = classLoader != null ? classLoader - : ClassLoaderUtils.getDefaultClassLoader(); - return (T) converter.get().convert(source, targetTypeToUse, classLoaderToUse); - } - catch (Exception ex) { - if (ex instanceof ConversionException) { - // simply rethrow it - throw (ConversionException) ex; - } - // else - throw new ConversionException( - String.format("Failed to convert String \"%s\" to type %s", source, targetType.getTypeName()), ex); - } - } + Converter converter = Stream.concat( // + StreamSupport.stream(serviceLoader.spliterator(), false), // + Stream.of(DefaultConverter.INSTANCE)) // + .filter(candidate -> candidate.canConvert(source, targetType)) // + .findFirst() // + .orElseThrow(() -> new ConversionException("No registered or built-in converter for source '" + source + + "' and target type " + targetType.getType().getTypeName())); - throw new ConversionException( - "No built-in converter for source type java.lang.String and target type " + targetType.getTypeName()); + return (T) converter.convert(source, targetType, classLoaderToUse); } - private static Class toWrapperType(Class targetType) { - Class wrapperType = getWrapperType(targetType); - return wrapperType != null ? wrapperType : targetType; + private static ClassLoader getClassLoader(ClassLoader classLoader) { + return classLoader != null ? classLoader : ClassLoaderUtils.getDefaultClassLoader(); } } diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/Converter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/Converter.java new file mode 100644 index 000000000000..640df9b5d702 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/Converter.java @@ -0,0 +1,68 @@ +/* + * 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.platform.commons.support.conversion; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import org.apiguardian.api.API; + +/** + * {@code Converter} is an abstraction that allows an input object to + * be converted to an instance of a different class. + * + *

Implementations are loaded via the {@link java.util.ServiceLoader} and must + * follow the service provider requirements. They should not make any assumptions + * regarding when they are instantiated or how often they are called. Since + * instances may potentially be cached and called from different threads, they + * should be thread-safe. + * + *

Extend {@link TypedConverter} if your implementation always converts + * from a given source type into a given target type and does not need access to + * the {@link ClassLoader} to perform the conversion. + * + * @since 1.13 + * @see ConversionSupport + * @see TypedConverter + */ +@API(status = EXPERIMENTAL, since = "1.13") +public interface Converter { + + /** + * Determine if the supplied source object can be converted into an instance + * of the specified target type. + * + * @param source the source object to convert; may be {@code null} but only + * if the target type is a reference type + * @param targetType the descriptor of the type the source should be converted into; + * never {@code null} + * @return {@code true} if the supplied source can be converted + */ + boolean canConvert(Object source, TypeDescriptor targetType); + + /** + * Convert the supplied source object into an instance of the specified + * target type. + * + *

This method will only be invoked if {@link #canConvert(Object, TypeDescriptor)} + * returned {@code true} for the same target type. + * + * @param source the source object to convert; may be {@code null} but only + * if the target type is a reference type + * @param targetType the descriptor of the type the source should be converted into; + * never {@code null} + * @param classLoader the {@code ClassLoader} to use; never {@code null} + * @return the converted object; may be {@code null} but only if the target + * type is a reference type + * @throws ConversionException if an error occurs during the conversion + */ + Object convert(Object source, TypeDescriptor targetType, ClassLoader classLoader) throws ConversionException; + +} diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/DefaultConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/DefaultConverter.java new file mode 100644 index 000000000000..0a438b642f10 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/DefaultConverter.java @@ -0,0 +1,181 @@ +/* + * 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.platform.commons.support.conversion; + +import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableList; +import static org.apiguardian.api.API.Status.INTERNAL; + +import java.io.File; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.URI; +import java.net.URL; +import java.util.Currency; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.UUID; + +import org.apiguardian.api.API; +import org.junit.platform.commons.util.ClassLoaderUtils; + +/** + * {@code DefaultConversionService} is the default implementation of the + * {@link Converter} API. + * + *

The {@code DefaultConversionService} is able to convert from strings to a + * number of primitive types and their corresponding wrapper types (Byte, Short, + * Integer, Long, Float, and Double), date and time types from the + * {@code java.time} package, and some additional common Java types such as + * {@link File}, {@link BigDecimal}, {@link BigInteger}, {@link Currency}, + * {@link Locale}, {@link URI}, {@link URL}, {@link UUID}, etc. + * + *

If the source and target types are identical, the source object will not + * be modified. + * + * @since 1.13 + */ +@API(status = INTERNAL, since = "1.13") +public class DefaultConverter implements Converter { + + static final DefaultConverter INSTANCE = new DefaultConverter(); + + private static final List stringToObjectConverters = unmodifiableList(asList( // + new StringToBooleanConverter(), // + new StringToCharacterConverter(), // + new StringToNumberConverter(), // + new StringToClassConverter(), // + new StringToEnumConverter(), // + new StringToJavaTimeConverter(), // + new StringToCommonJavaTypesConverter(), // + new FallbackStringToObjectConverter() // + )); + + private DefaultConverter() { + // nothing to initialize + } + + /** + * Determine if the supplied source object can be converted into an instance + * of the specified target type. + * + * @param source the source object to convert; may be {@code null} but only + * if the target type is a reference type + * @param targetType the target type the source should be converted into; + * never {@code null} + * @return {@code true} if the supplied source can be converted + */ + @Override + public boolean canConvert(Object source, TypeDescriptor targetType) { + if (source == null) { + return !targetType.isPrimitive(); + } + + if (!(source instanceof String)) { + return false; + } + + if (String.class.equals(targetType.getType())) { + return true; + } + + return stringToObjectConverters.stream().anyMatch( + candidate -> candidate.canConvert(targetType.getWrapperType())); + } + + /** + * Convert the supplied source object into an instance of the specified + * target type. + * + *

If the target type is {@code String}, the source {@code String} will not + * be modified. + * + *

Some forms of conversion require a {@link ClassLoader}. If none is + * provided, the {@linkplain ClassLoaderUtils#getDefaultClassLoader() default + * ClassLoader} will be used. + * + *

This method is able to convert strings into primitive types and their + * corresponding wrapper types ({@link Boolean}, {@link Character}, {@link Byte}, + * {@link Short}, {@link Integer}, {@link Long}, {@link Float}, and + * {@link Double}), enum constants, date and time types from the + * {@code java.time} package, as well as common Java types such as {@link Class}, + * {@link java.io.File}, {@link java.nio.file.Path}, {@link java.nio.charset.Charset}, + * {@link java.math.BigDecimal}, {@link java.math.BigInteger}, + * {@link java.util.Currency}, {@link java.util.Locale}, {@link java.util.UUID}, + * {@link java.net.URI}, and {@link java.net.URL}. + * + *

If the target type is not covered by any of the above, a convention-based + * conversion strategy will be used to convert the source {@code String} into the + * given target type by invoking a static factory method or factory constructor + * defined in the target type. The search algorithm used in this strategy is + * outlined below. + * + *

Search Algorithm

+ * + *
    + *
  1. Search for a single, non-private static factory method in the target + * type that converts from a String to the target type. Use the factory method + * if present.
  2. + *
  3. Search for a single, non-private constructor in the target type that + * accepts a String. Use the constructor if present.
  4. + *
+ * + *

If multiple suitable factory methods are discovered, they will be ignored. + * If neither a single factory method nor a single constructor is found, the + * convention-based conversion strategy will not apply. + * + * @param source the source object to convert; may be {@code null} but only + * if the target type is a reference type + * @param targetType the target type the source should be converted into; + * never {@code null} + * @param classLoader the {@code ClassLoader} to use; never {@code null} + * @return the converted object; may be {@code null} but only if the target + * type is a reference type + * @throws ConversionException if an error occurs during the conversion + */ + @Override + public Object convert(Object source, TypeDescriptor targetType, ClassLoader classLoader) { + if (source == null) { + if (targetType.isPrimitive()) { + throw new ConversionException( + "Cannot convert null to primitive value of type " + targetType.getType().getTypeName()); + } + return null; + } + + if (String.class.equals(targetType.getType())) { + return source; + } + + Class targetTypeToUse = targetType.getWrapperType(); + Optional converter = stringToObjectConverters.stream().filter( + candidate -> candidate.canConvert(targetTypeToUse)).findFirst(); + if (converter.isPresent()) { + try { + return converter.get().convert((String) source, targetTypeToUse, classLoader); + } + catch (Exception ex) { + if (ex instanceof ConversionException) { + // simply rethrow it + throw (ConversionException) ex; + } + // else + throw new ConversionException(String.format("Failed to convert String \"%s\" to type %s", source, + targetType.getType().getTypeName()), ex); + } + } + + throw new ConversionException("No built-in converter for source type java.lang.String and target type " + + targetType.getType().getTypeName()); + } + +} diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/FallbackStringToObjectConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/FallbackStringToObjectConverter.java index aeea2d9e119d..642d3ae74b37 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/FallbackStringToObjectConverter.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/FallbackStringToObjectConverter.java @@ -51,7 +51,7 @@ * @since 1.11 * @see ConversionSupport */ -class FallbackStringToObjectConverter implements StringToObjectConverter { +class FallbackStringToObjectConverter extends StringToObjectConverter { /** * Implementation of the NULL Object Pattern. @@ -70,12 +70,12 @@ class FallbackStringToObjectConverter implements StringToObjectConverter { = new ConcurrentHashMap<>(64); @Override - public boolean canConvertTo(Class targetType) { + public boolean canConvert(Class targetType) { return findFactoryExecutable(targetType) != NULL_EXECUTABLE; } @Override - public Object convert(String source, Class targetType) throws Exception { + public Object convert(String source, Class targetType, ClassLoader classLoader) { Function executable = findFactoryExecutable(targetType); Preconditions.condition(executable != NULL_EXECUTABLE, "Illegal state: convert() must not be called if canConvert() returned false"); diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToBooleanConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToBooleanConverter.java index 4bfefc7b48b1..3d9ec2392adf 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToBooleanConverter.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToBooleanConverter.java @@ -12,15 +12,15 @@ import org.junit.platform.commons.util.Preconditions; -class StringToBooleanConverter implements StringToObjectConverter { +class StringToBooleanConverter extends StringToObjectConverter { @Override - public boolean canConvertTo(Class targetType) { + public boolean canConvert(Class targetType) { return targetType == Boolean.class; } @Override - public Object convert(String source, Class targetType) { + public Object convert(String source, Class targetType, ClassLoader classLoader) { boolean isTrue = "true".equalsIgnoreCase(source); Preconditions.condition(isTrue || "false".equalsIgnoreCase(source), () -> "String must be 'true' or 'false' (ignoring case): " + source); diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToCharacterConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToCharacterConverter.java index 0f5729a228fc..61f56fcc38ab 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToCharacterConverter.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToCharacterConverter.java @@ -12,15 +12,15 @@ import org.junit.platform.commons.util.Preconditions; -class StringToCharacterConverter implements StringToObjectConverter { +class StringToCharacterConverter extends StringToObjectConverter { @Override - public boolean canConvertTo(Class targetType) { + public boolean canConvert(Class targetType) { return targetType == Character.class; } @Override - public Object convert(String source, Class targetType) { + public Object convert(String source, Class targetType, ClassLoader classLoader) { Preconditions.condition(source.length() == 1, () -> "String must have length of 1: " + source); return source.charAt(0); } diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToClassConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToClassConverter.java index ad16fb18edf3..4e47344b0214 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToClassConverter.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToClassConverter.java @@ -12,20 +12,15 @@ import org.junit.platform.commons.support.ReflectionSupport; -class StringToClassConverter implements StringToObjectConverter { +class StringToClassConverter extends StringToObjectConverter { @Override - public boolean canConvertTo(Class targetType) { + public boolean canConvert(Class targetType) { return targetType == Class.class; } @Override - public Object convert(String source, Class targetType) throws Exception { - throw new UnsupportedOperationException("Invoke convert(String, Class, ClassLoader) instead"); - } - - @Override - public Object convert(String className, Class targetType, ClassLoader classLoader) throws Exception { + public Object convert(String className, Class targetType, ClassLoader classLoader) { // @formatter:off return ReflectionSupport.tryToLoadClass(className, classLoader) .getOrThrow(cause -> new ConversionException( diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToCommonJavaTypesConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToCommonJavaTypesConverter.java index 17aa357439ae..9d68067ff4ef 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToCommonJavaTypesConverter.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToCommonJavaTypesConverter.java @@ -26,7 +26,7 @@ import java.util.UUID; import java.util.function.Function; -class StringToCommonJavaTypesConverter implements StringToObjectConverter { +class StringToCommonJavaTypesConverter extends StringToObjectConverter { private static final Map, Function> CONVERTERS; @@ -49,12 +49,12 @@ class StringToCommonJavaTypesConverter implements StringToObjectConverter { } @Override - public boolean canConvertTo(Class targetType) { + public boolean canConvert(Class targetType) { return CONVERTERS.containsKey(targetType); } @Override - public Object convert(String source, Class targetType) throws Exception { + public Object convert(String source, Class targetType, ClassLoader classLoader) { return CONVERTERS.get(targetType).apply(source); } diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToEnumConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToEnumConverter.java index ee18f8f8b1e3..f9202dffaaaa 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToEnumConverter.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToEnumConverter.java @@ -10,16 +10,16 @@ package org.junit.platform.commons.support.conversion; -class StringToEnumConverter implements StringToObjectConverter { +class StringToEnumConverter extends StringToObjectConverter { @Override - public boolean canConvertTo(Class targetType) { + public boolean canConvert(Class targetType) { return targetType.isEnum(); } @Override @SuppressWarnings({ "unchecked", "rawtypes" }) - public Object convert(String source, Class targetType) throws Exception { + public Object convert(String source, Class targetType, ClassLoader classLoader) { return Enum.valueOf(targetType, source); } diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToJavaTimeConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToJavaTimeConverter.java index 0e544f39a3a4..150600952386 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToJavaTimeConverter.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToJavaTimeConverter.java @@ -30,7 +30,7 @@ import java.util.Map; import java.util.function.Function; -class StringToJavaTimeConverter implements StringToObjectConverter { +class StringToJavaTimeConverter extends StringToObjectConverter { private static final Map, Function> CONVERTERS; static { @@ -53,12 +53,12 @@ class StringToJavaTimeConverter implements StringToObjectConverter { } @Override - public boolean canConvertTo(Class targetType) { + public boolean canConvert(Class targetType) { return CONVERTERS.containsKey(targetType); } @Override - public Object convert(String source, Class targetType) throws Exception { + public Object convert(String source, Class targetType, ClassLoader classLoader) { return CONVERTERS.get(targetType).apply(source); } diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToNumberConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToNumberConverter.java index 21a7a0fc2e62..2bd35e16abb0 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToNumberConverter.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToNumberConverter.java @@ -18,7 +18,7 @@ import java.util.Map; import java.util.function.Function; -class StringToNumberConverter implements StringToObjectConverter { +class StringToNumberConverter extends StringToObjectConverter { private static final Map, Function> CONVERTERS; static { @@ -38,12 +38,12 @@ class StringToNumberConverter implements StringToObjectConverter { } @Override - public boolean canConvertTo(Class targetType) { + public boolean canConvert(Class targetType) { return CONVERTERS.containsKey(targetType); } @Override - public Object convert(String source, Class targetType) { + public Object convert(String source, Class targetType, ClassLoader classLoader) { return CONVERTERS.get(targetType).apply(source.replace("_", "")); } diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToObjectConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToObjectConverter.java index ceb00e2e95d8..821e5114b443 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToObjectConverter.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToObjectConverter.java @@ -14,39 +14,33 @@ * Internal API for converting arguments of type {@link String} to a specified * target type. */ -interface StringToObjectConverter { +abstract class StringToObjectConverter implements Converter { + + @Override + public final boolean canConvert(Object source, TypeDescriptor targetType) { + return canConvert(targetType.getType()); + } /** * Determine if this converter can convert from a {@link String} to the * supplied target type (which is guaranteed to be a wrapper type for * primitives — for example, {@link Integer} instead of {@code int}). */ - boolean canConvertTo(Class targetType); + abstract boolean canConvert(Class targetType); - /** - * Convert the supplied {@link String} to the supplied target type (which is - * guaranteed to be a wrapper type for primitives — for example, - * {@link Integer} instead of {@code int}). - * - *

This method will only be invoked in {@link #canConvertTo(Class)} - * returned {@code true} for the same target type. - */ - Object convert(String source, Class targetType) throws Exception; + @Override + public final Object convert(Object source, TypeDescriptor targetType, ClassLoader classLoader) { + return convert((String) source, targetType.getType(), classLoader); + } /** * Convert the supplied {@link String} to the supplied target type (which is * guaranteed to be a wrapper type for primitives — for example, * {@link Integer} instead of {@code int}). * - *

This method will only be invoked in {@link #canConvertTo(Class)} + *

This method will only be invoked if {@link #canConvert(Class)} * returned {@code true} for the same target type. - * - *

The default implementation simply delegates to {@link #convert(String, Class)}. - * Can be overridden by concrete implementations of this interface that need - * access to the supplied {@link ClassLoader}. */ - default Object convert(String source, Class targetType, ClassLoader classLoader) throws Exception { - return convert(source, targetType); - } + abstract Object convert(String source, Class targetType, ClassLoader classLoader); } diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/TypeDescriptor.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/TypeDescriptor.java new file mode 100644 index 000000000000..f572d33927c0 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/TypeDescriptor.java @@ -0,0 +1,77 @@ +/* + * 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.platform.commons.support.conversion; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.lang.reflect.Field; +import java.lang.reflect.Parameter; + +import org.apiguardian.api.API; +import org.junit.platform.commons.util.ReflectionUtils; + +/** + * + * + * @since 1.13 + */ +@API(status = EXPERIMENTAL, since = "1.13") +public final class TypeDescriptor { + + private final Class type; + + public static TypeDescriptor forType(Class type) { + return new TypeDescriptor(type); + } + + public static TypeDescriptor forField(Field field) { + return new TypeDescriptor(field.getType()); + } + + public static TypeDescriptor forParameter(Parameter parameter) { + return new TypeDescriptor(parameter.getType()); + } + + private TypeDescriptor(Class type) { + this.type = type; + } + + public Class getType() { + return type; + } + + public Class getWrapperType() { + Class wrapperType = ReflectionUtils.getWrapperType(type); + return wrapperType != null ? wrapperType : type; + } + + public boolean isPrimitive() { + return getType().isPrimitive(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TypeDescriptor that = (TypeDescriptor) o; + return this.type.equals(that.type); + } + + @Override + public int hashCode() { + return this.type.hashCode(); + } + +} diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/TypedConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/TypedConverter.java new file mode 100644 index 000000000000..ebbd91ed778e --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/TypedConverter.java @@ -0,0 +1,68 @@ +/* + * 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.platform.commons.support.conversion; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import org.apiguardian.api.API; +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.commons.util.ReflectionUtils; + +/** + * {@code TypedConversionService} is an abstract base class for + * {@link Converter} implementations that always convert objects of a + * given source type into a given target type. + * + * @param the type of the source argument to convert + * @param the type of the target object to create from the source + * @since 1.13 + */ +@API(status = EXPERIMENTAL, since = "1.13") +public abstract class TypedConverter implements Converter { + + private final Class sourceType; + private final Class targetType; + + /** + * Create a new {@code TypedConversionService}. + * + * @param sourceType the type of the argument to convert; never {@code null} + * @param targetType the type of the target object to create from the source; + * never {@code null} + */ + protected TypedConverter(Class sourceType, Class targetType) { + this.sourceType = Preconditions.notNull(sourceType, "sourceType must not be null"); + this.targetType = Preconditions.notNull(targetType, "targetType must not be null"); + } + + @Override + public final boolean canConvert(Object source, TypeDescriptor targetType) { + return this.sourceType.isInstance(source) + && ReflectionUtils.isAssignableTo(this.targetType, targetType.getType()); + } + + @Override + public final Object convert(Object source, TypeDescriptor targetType, ClassLoader classLoader) { + return source == null ? convert(null) : convert(this.sourceType.cast(source)); + } + + /** + * Convert the supplied {@code source} object of type {@code S} into an object + * of type {@code T}. + * + * @param source the source object to convert; may be {@code null} + * @return the converted object; may be {@code null} but only if the target + * type is a reference type + * @throws ConversionException if an error occurs during the conversion + */ + protected abstract T convert(S source) throws ConversionException; + +} diff --git a/junit-platform-commons/src/module/org.junit.platform.commons/module-info.java b/junit-platform-commons/src/module/org.junit.platform.commons/module-info.java index bb545c577f82..37ed2dbf3482 100644 --- a/junit-platform-commons/src/module/org.junit.platform.commons/module-info.java +++ b/junit-platform-commons/src/module/org.junit.platform.commons/module-info.java @@ -54,5 +54,6 @@ org.junit.platform.suite.engine, org.junit.platform.testkit, org.junit.vintage.engine; + uses org.junit.platform.commons.support.conversion.Converter; uses org.junit.platform.commons.support.scanning.ClasspathScanner; } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/converter/DefaultArgumentConverterTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/converter/DefaultArgumentConverterTests.java index 501f4c09a40c..6aabf1cce5c1 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/converter/DefaultArgumentConverterTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/converter/DefaultArgumentConverterTests.java @@ -35,6 +35,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.junit.platform.commons.support.ReflectionSupport; import org.junit.platform.commons.support.conversion.ConversionException; +import org.junit.platform.commons.support.conversion.TypeDescriptor; import org.junit.platform.commons.test.TestClassLoader; import org.junit.platform.commons.util.ClassLoaderUtils; @@ -89,26 +90,17 @@ void throwsExceptionForNullToPrimitiveTypeConversion(Class type) { .isThrownBy(() -> convert(null, type)) // .withMessage("Cannot convert null to primitive value of type " + type.getCanonicalName()); - verify(underTest, never()).convert(any(), any(), any(ClassLoader.class)); - } - - @Test - void throwsExceptionForNonStringsConversion() { - assertThatExceptionOfType(ArgumentConversionException.class) // - .isThrownBy(() -> convert(new Enigma(), String.class)) // - .withMessage("No built-in converter for source type %s and target type java.lang.String", - Enigma.class.getName()); - - verify(underTest, never()).convert(any(), any(), any(ClassLoader.class)); + verify(underTest, never()).delegateConversion(any(), any(), any(ClassLoader.class)); } @Test void delegatesStringsConversion() { - doReturn(null).when(underTest).convert(any(), any(), any(ClassLoader.class)); + doReturn(null).when(underTest).delegateConversion(any(), any(), any(ClassLoader.class)); convert("value", int.class); - verify(underTest).convert("value", int.class, getClassLoader(DefaultArgumentConverterTests.class)); + verify(underTest).delegateConversion("value", TypeDescriptor.forType(int.class), + getClassLoader(DefaultArgumentConverterTests.class)); } @Test @@ -138,20 +130,22 @@ void delegatesLocaleConversionWithExplicitIso639Format() { convert("en", Locale.class); - verify(underTest).convert("en", Locale.class, getClassLoader(DefaultArgumentConverterTests.class)); + verify(underTest).convert("en", TypeDescriptor.forType(Locale.class), + getClassLoader(DefaultArgumentConverterTests.class)); } @Test void throwsExceptionForDelegatedConversionFailure() { ConversionException exception = new ConversionException("fail"); - doThrow(exception).when(underTest).convert(any(), any(), any(ClassLoader.class)); + doThrow(exception).when(underTest).delegateConversion(any(), any(), any(ClassLoader.class)); assertThatExceptionOfType(ArgumentConversionException.class) // .isThrownBy(() -> convert("value", int.class)) // .withCause(exception) // .withMessage(exception.getMessage()); - verify(underTest).convert("value", int.class, getClassLoader(DefaultArgumentConverterTests.class)); + verify(underTest).delegateConversion("value", TypeDescriptor.forType(int.class), + getClassLoader(DefaultArgumentConverterTests.class)); } @Test @@ -164,14 +158,14 @@ void delegatesStringToClassWithCustomTypeFromDifferentClassLoaderConversion() th var declaringExecutable = ReflectionSupport.findMethod(customType, "foo").orElseThrow(); assertThat(declaringExecutable.getDeclaringClass().getClassLoader()).isSameAs(testClassLoader); - doReturn(customType).when(underTest).convert(any(), any(), any(ClassLoader.class)); + doReturn(customType).when(underTest).delegateConversion(any(), any(), any(ClassLoader.class)); var clazz = (Class) convert(customTypeName, Class.class, testClassLoader); assertThat(clazz).isNotEqualTo(Enigma.class); assertThat(clazz).isEqualTo(customType); assertThat(clazz.getClassLoader()).isSameAs(testClassLoader); - verify(underTest).convert(customTypeName, Class.class, testClassLoader); + verify(underTest).delegateConversion(customTypeName, TypeDescriptor.forType(Class.class), testClassLoader); } } @@ -184,7 +178,7 @@ private void assertConverts(Object input, Class targetClass, Object expectedO .describedAs(input + " --(" + targetClass.getName() + ")--> " + expectedOutput) // .isEqualTo(expectedOutput); - verify(underTest, never()).convert(any(), any(), any(ClassLoader.class)); + verify(underTest, never()).delegateConversion(any(), any(), any(ClassLoader.class)); } private Object convert(Object input, Class targetClass) { @@ -192,7 +186,7 @@ private Object convert(Object input, Class targetClass) { } private Object convert(Object input, Class targetClass, ClassLoader classLoader) { - return underTest.convert(input, targetClass, classLoader); + return underTest.convert(input, TypeDescriptor.forType(targetClass), classLoader); } @SuppressWarnings("unused") diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/converter/LocaleConverter.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/converter/LocaleConverter.java new file mode 100644 index 000000000000..95e6e619ca83 --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/converter/LocaleConverter.java @@ -0,0 +1,29 @@ +/* + * 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.converter; + +import java.util.Locale; + +import org.junit.platform.commons.support.conversion.TypedConverter; + +// FIXME delete +public class LocaleConverter extends TypedConverter { + + public LocaleConverter() { + super(String.class, Locale.class); + } + + @Override + protected Locale convert(String source) { + return Locale.forLanguageTag(source); + } + +} diff --git a/jupiter-tests/src/test/resources/META-INF/services/org.junit.platform.commons.support.conversion.Converter b/jupiter-tests/src/test/resources/META-INF/services/org.junit.platform.commons.support.conversion.Converter new file mode 100644 index 000000000000..562269b9c9d3 --- /dev/null +++ b/jupiter-tests/src/test/resources/META-INF/services/org.junit.platform.commons.support.conversion.Converter @@ -0,0 +1 @@ +org.junit.jupiter.params.converter.LocaleConverter diff --git a/platform-tests/src/test/java/org/junit/platform/commons/support/conversion/ConversionSupportTests.java b/platform-tests/src/test/java/org/junit/platform/commons/support/conversion/DefaultConverterTests.java similarity index 83% rename from platform-tests/src/test/java/org/junit/platform/commons/support/conversion/ConversionSupportTests.java rename to platform-tests/src/test/java/org/junit/platform/commons/support/conversion/DefaultConverterTests.java index 3a57fbe3b5a3..f16724fd9fbf 100644 --- a/platform-tests/src/test/java/org/junit/platform/commons/support/conversion/ConversionSupportTests.java +++ b/platform-tests/src/test/java/org/junit/platform/commons/support/conversion/DefaultConverterTests.java @@ -50,11 +50,11 @@ import org.junit.platform.commons.util.ClassLoaderUtils; /** - * Unit tests for {@link ConversionSupport}. + * Unit tests for {@link DefaultConverter}. * - * @since 5.12 + * @since 1.13 */ -class ConversionSupportTests { +class DefaultConverterTests { @Test void isAwareOfNull() { @@ -105,45 +105,61 @@ void convertsStringsToPrimitiveWrapperTypes() { @ValueSource(classes = { char.class, boolean.class, short.class, byte.class, int.class, long.class, float.class, double.class, void.class }) void throwsExceptionForNullToPrimitiveTypeConversion(Class type) { + TypeDescriptor typeDescriptor = TypeDescriptor.forType(type); + + assertThat(canConvert(null, typeDescriptor)).isFalse(); assertThatExceptionOfType(ConversionException.class) // - .isThrownBy(() -> convert(null, type)) // - .withMessage("Cannot convert null to primitive value of type " + type.getCanonicalName()); + .isThrownBy(() -> convert(null, typeDescriptor)) // + .withMessage("Cannot convert null to primitive value of type %s", type.getCanonicalName()); } @ParameterizedTest(name = "[{index}] {0}") @ValueSource(classes = { Boolean.class, Character.class, Short.class, Byte.class, Integer.class, Long.class, Float.class, Double.class }) void throwsExceptionWhenConvertingTheWordNullToPrimitiveWrapperType(Class type) { + TypeDescriptor typeDescriptor = TypeDescriptor.forType(type); + + assertThat(canConvert("null", typeDescriptor)).isTrue(); assertThatExceptionOfType(ConversionException.class) // - .isThrownBy(() -> convert("null", type)) // + .isThrownBy(() -> convert("null", typeDescriptor)) // .withMessage("Failed to convert String \"null\" to type " + type.getCanonicalName()); + + assertThat(canConvert("NULL", typeDescriptor)).isTrue(); assertThatExceptionOfType(ConversionException.class) // - .isThrownBy(() -> convert("NULL", type)) // + .isThrownBy(() -> convert("NULL", typeDescriptor)) // .withMessage("Failed to convert String \"NULL\" to type " + type.getCanonicalName()); } @Test void throwsExceptionOnInvalidStringForPrimitiveTypes() { + TypeDescriptor charDescriptor = TypeDescriptor.forType(char.class); + + assertThat(canConvert("ab", charDescriptor)).isTrue(); assertThatExceptionOfType(ConversionException.class) // - .isThrownBy(() -> convert("ab", char.class)) // + .isThrownBy(() -> convert("ab", charDescriptor)) // .withMessage("Failed to convert String \"ab\" to type char") // .havingCause() // .withMessage("String must have length of 1: ab"); + TypeDescriptor booleanDescriptor = TypeDescriptor.forType(boolean.class); + + assertThat(canConvert("tru", booleanDescriptor)).isTrue(); assertThatExceptionOfType(ConversionException.class) // - .isThrownBy(() -> convert("tru", boolean.class)) // + .isThrownBy(() -> convert("tru", booleanDescriptor)) // .withMessage("Failed to convert String \"tru\" to type boolean") // .havingCause() // .withMessage("String must be 'true' or 'false' (ignoring case): tru"); + assertThat(canConvert("null", booleanDescriptor)).isTrue(); assertThatExceptionOfType(ConversionException.class) // - .isThrownBy(() -> convert("null", boolean.class)) // + .isThrownBy(() -> convert("null", booleanDescriptor)) // .withMessage("Failed to convert String \"null\" to type boolean") // .havingCause() // .withMessage("String must be 'true' or 'false' (ignoring case): null"); + assertThat(canConvert("NULL", booleanDescriptor)).isTrue(); assertThatExceptionOfType(ConversionException.class) // - .isThrownBy(() -> convert("NULL", boolean.class)) // + .isThrownBy(() -> convert("NULL", booleanDescriptor)) // .withMessage("Failed to convert String \"NULL\" to type boolean") // .havingCause() // .withMessage("String must be 'true' or 'false' (ignoring case): NULL"); @@ -151,8 +167,11 @@ void throwsExceptionOnInvalidStringForPrimitiveTypes() { @Test void throwsExceptionWhenImplicitConversionIsUnsupported() { + TypeDescriptor typeDescriptor = TypeDescriptor.forType(Enigma.class); + + assertThat(canConvert("foo", typeDescriptor)).isFalse(); assertThatExceptionOfType(ConversionException.class) // - .isThrownBy(() -> convert("foo", Enigma.class)) // + .isThrownBy(() -> convert("foo", typeDescriptor)) // .withMessage("No built-in converter for source type java.lang.String and target type %s", Enigma.class.getName()); } @@ -232,7 +251,10 @@ void convertsStringToClassWithCustomTypeFromDifferentClassLoader() throws Except var declaringExecutable = ReflectionSupport.findMethod(customType, "foo").get(); assertThat(declaringExecutable.getDeclaringClass().getClassLoader()).isSameAs(testClassLoader); - var clazz = (Class) convert(customTypeName, Class.class, classLoader(declaringExecutable)); + var typeDescriptor = TypeDescriptor.forType(Class.class); + assertThat(canConvert(customTypeName, typeDescriptor)).isTrue(); + + var clazz = (Class) convert(customTypeName, typeDescriptor, classLoader(declaringExecutable)); assertThat(clazz).isNotEqualTo(Enigma.class); assertThat(clazz).isEqualTo(customType); assertThat(clazz.getClassLoader()).isSameAs(testClassLoader); @@ -309,24 +331,32 @@ void convertsStringToUUID() { // ------------------------------------------------------------------------- - private void assertConverts(String input, Class targetClass, Object expectedOutput) { - var result = convert(input, targetClass); + private void assertConverts(Object input, Class targetClass, Object expectedOutput) { + TypeDescriptor typeDescriptor = TypeDescriptor.forType(targetClass); + + assertThat(canConvert(input, typeDescriptor)).isTrue(); + + var result = convert(input, typeDescriptor); assertThat(result) // .describedAs(input + " --(" + targetClass.getName() + ")--> " + expectedOutput) // .isEqualTo(expectedOutput); } - private Object convert(String input, Class targetClass) { + private boolean canConvert(Object input, TypeDescriptor targetClass) { + return DefaultConverter.INSTANCE.canConvert(input, targetClass); + } + + private Object convert(Object input, TypeDescriptor targetClass) { return convert(input, targetClass, classLoader()); } - private Object convert(String input, Class targetClass, ClassLoader classLoader) { - return ConversionSupport.convert(input, targetClass, classLoader); + private Object convert(Object input, TypeDescriptor targetClass, ClassLoader classLoader) { + return DefaultConverter.INSTANCE.convert(input, targetClass, classLoader); } private static ClassLoader classLoader() { - Method declaringExecutable = ReflectionSupport.findMethod(ConversionSupportTests.class, "foo").get(); + Method declaringExecutable = ReflectionSupport.findMethod(DefaultConverterTests.class, "foo").get(); return classLoader(declaringExecutable); } diff --git a/platform-tests/src/test/java/org/junit/platform/commons/support/conversion/FallbackStringToObjectConverterTests.java b/platform-tests/src/test/java/org/junit/platform/commons/support/conversion/FallbackStringToObjectConverterTests.java index cc2e8b38469d..a2121c366921 100644 --- a/platform-tests/src/test/java/org/junit/platform/commons/support/conversion/FallbackStringToObjectConverterTests.java +++ b/platform-tests/src/test/java/org/junit/platform/commons/support/conversion/FallbackStringToObjectConverterTests.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; import org.junit.platform.commons.support.conversion.FallbackStringToObjectConverter.IsFactoryConstructor; import org.junit.platform.commons.support.conversion.FallbackStringToObjectConverter.IsFactoryMethod; +import org.junit.platform.commons.util.ClassLoaderUtils; import org.junit.platform.commons.util.ReflectionUtils; /** @@ -87,13 +88,13 @@ void convertsStringToNewspaperViaConstructorIgnoringMultipleFactoryMethods() thr @Test @DisplayName("Cannot convert String to Diary because Diary has neither a static factory method nor a factory constructor") void cannotConvertStringToDiary() { - assertThat(converter.canConvertTo(Diary.class)).isFalse(); + assertThat(converter.canConvert(Diary.class)).isFalse(); } @Test @DisplayName("Cannot convert String to Magazine because Magazine has multiple static factory methods") void cannotConvertStringToMagazine() { - assertThat(converter.canConvertTo(Magazine.class)).isFalse(); + assertThat(converter.canConvert(Magazine.class)).isFalse(); } // ------------------------------------------------------------------------- @@ -120,15 +121,19 @@ private static Method magazineMethod(String methodName) { } private static void assertConverts(String input, Class targetType, Object expectedOutput) throws Exception { - assertThat(converter.canConvertTo(targetType)).isTrue(); + assertThat(converter.canConvert(targetType)).isTrue(); - var result = converter.convert(input, targetType); + var result = converter.convert(input, targetType, classLoader()); assertThat(result) // .describedAs(input + " --(" + targetType.getName() + ")--> " + expectedOutput) // .isEqualTo(expectedOutput); } + private static ClassLoader classLoader() { + return ClassLoaderUtils.getClassLoader(FallbackStringToObjectConverterTests.class); + } + static class Book { private final String title; diff --git a/platform-tooling-support-tests/projects/jar-describe-module/junit-platform-commons.expected.txt b/platform-tooling-support-tests/projects/jar-describe-module/junit-platform-commons.expected.txt index e8104a7e3dd8..df664572ef8b 100644 --- a/platform-tooling-support-tests/projects/jar-describe-module/junit-platform-commons.expected.txt +++ b/platform-tooling-support-tests/projects/jar-describe-module/junit-platform-commons.expected.txt @@ -9,6 +9,7 @@ requires java.base mandated requires java.logging requires java.management requires org.apiguardian.api static transitive +uses org.junit.platform.commons.support.conversion.Converter uses org.junit.platform.commons.support.scanning.ClasspathScanner qualified exports org.junit.platform.commons.logging to org.junit.jupiter.api org.junit.jupiter.engine org.junit.jupiter.migrationsupport org.junit.jupiter.params org.junit.platform.console org.junit.platform.engine org.junit.platform.launcher org.junit.platform.reporting org.junit.platform.runner org.junit.platform.suite.api org.junit.platform.suite.engine org.junit.platform.testkit org.junit.vintage.engine qualified exports org.junit.platform.commons.util to org.junit.jupiter.api org.junit.jupiter.engine org.junit.jupiter.migrationsupport org.junit.jupiter.params org.junit.platform.console org.junit.platform.engine org.junit.platform.jfr org.junit.platform.launcher org.junit.platform.reporting org.junit.platform.runner org.junit.platform.suite.api org.junit.platform.suite.commons org.junit.platform.suite.engine org.junit.platform.testkit org.junit.vintage.engine