From 42cc962c73277a3996037f2846ea6577747d5de8 Mon Sep 17 00:00:00 2001 From: Stefano Cordio Date: Mon, 23 Dec 2024 02:31:04 +0100 Subject: [PATCH 1/4] Introduce `Converter` in `junit-platform-commons` --- .../aggregator/DefaultArgumentsAccessor.java | 4 +- .../converter/DefaultArgumentConverter.java | 37 ++-- .../src/main/java/module-info.java | 1 + .../support/conversion/ConversionContext.java | 50 ++++++ .../support/conversion/ConversionSupport.java | 126 ++++--------- .../commons/support/conversion/Converter.java | 65 +++++++ .../support/conversion/DefaultConverter.java | 165 ++++++++++++++++++ .../FallbackStringToObjectConverter.java | 9 +- .../conversion/StringToBooleanConverter.java | 6 +- .../StringToCharacterConverter.java | 6 +- .../conversion/StringToClassConverter.java | 17 +- .../StringToCommonJavaTypesConverter.java | 6 +- .../conversion/StringToEnumConverter.java | 9 +- .../conversion/StringToJavaTimeConverter.java | 6 +- .../conversion/StringToNumberConverter.java | 10 +- .../StringToTargetTypeConverter.java | 47 +++++ ...java => StringToWrapperTypeConverter.java} | 40 ++--- .../support/conversion/TypeDescriptor.java | 113 ++++++++++++ .../support/conversion/TypedConverter.java | 68 ++++++++ .../DefaultArgumentConverterTests.java | 39 ++--- .../params/converter/LocaleConverter.java | 30 ++++ ...tform.commons.support.conversion.Converter | 1 + ...tTests.java => DefaultConverterTests.java} | 73 ++++++-- .../FallbackStringToObjectConverterTests.java | 6 +- .../conversion/TypeDescriptorTests.java | 28 +++ .../junit-platform-commons.expected.txt | 1 + 26 files changed, 754 insertions(+), 209 deletions(-) create mode 100644 junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionContext.java create mode 100644 junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/Converter.java create mode 100644 junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/DefaultConverter.java create mode 100644 junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToTargetTypeConverter.java rename junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/{StringToObjectConverter.java => StringToWrapperTypeConverter.java} (51%) create mode 100644 junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/TypeDescriptor.java create mode 100644 junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/TypedConverter.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/params/converter/LocaleConverter.java create mode 100644 jupiter-tests/src/test/resources/META-INF/services/org.junit.platform.commons.support.conversion.Converter rename platform-tests/src/test/java/org/junit/platform/commons/support/conversion/{ConversionSupportTests.java => DefaultConverterTests.java} (82%) create mode 100644 platform-tests/src/test/java/org/junit/platform/commons/support/conversion/TypeDescriptorTests.java 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 0128da8fd153..f20c9154425d 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 @@ -20,6 +20,7 @@ import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; 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; @@ -45,7 +46,8 @@ public static DefaultArgumentsAccessor create(int invocationIndex, ClassLoader c Preconditions.notNull(classLoader, "ClassLoader must not be null"); BiFunction<@Nullable Object, Class, @Nullable Object> converter = (source, - targetType) -> DefaultArgumentConverter.INSTANCE.convert(source, targetType, classLoader); + targetType) -> DefaultArgumentConverter.INSTANCE.convert(source, TypeDescriptor.forClass(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 260979b8de63..877597706685 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 @@ -26,8 +26,10 @@ import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.params.support.FieldContext; +import org.junit.platform.commons.support.conversion.ConversionContext; 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; /** @@ -41,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 @@ -58,49 +60,42 @@ private DefaultArgumentConverter() { @Override public final @Nullable Object convert(@Nullable 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 @Nullable Object convert(@Nullable 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 @Nullable Object convert(@Nullable Object source, Class targetType, ClassLoader classLoader) { + public final @Nullable Object convert(@Nullable 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 string) { - try { - return convert(string, targetType, classLoader); - } - catch (ConversionException ex) { - throw new ArgumentConversionException(ex.getMessage(), ex); - } + try { + ConversionContext context = new ConversionContext(source, targetType, classLoader); + return delegateConversion(source, context); + } + catch (ConversionException ex) { + throw new ArgumentConversionException(ex.getMessage(), ex); } - - throw new ArgumentConversionException("No built-in converter for source type %s and target type %s".formatted( - source.getClass().getTypeName(), targetType.getTypeName())); } @Nullable - Object convert(@Nullable String source, Class targetType, ClassLoader classLoader) { - return ConversionSupport.convert(source, targetType, classLoader); + Object delegateConversion(@Nullable Object source, ConversionContext context) { + return ConversionSupport.convert(source, context); } } diff --git a/junit-platform-commons/src/main/java/module-info.java b/junit-platform-commons/src/main/java/module-info.java index 3b83d2ffc150..63303fef0e1d 100644 --- a/junit-platform-commons/src/main/java/module-info.java +++ b/junit-platform-commons/src/main/java/module-info.java @@ -56,5 +56,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/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionContext.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionContext.java new file mode 100644 index 000000000000..42b0cbf42e95 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionContext.java @@ -0,0 +1,50 @@ +/* + * 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 static org.junit.platform.commons.util.ClassLoaderUtils.getDefaultClassLoader; + +import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; + +/** + * {@code ConversionContext} encapsulates the context in which the + * current conversion is being executed. + * + *

{@link Converter Converters} are provided an instance of + * {@code ConversionContext} to perform their work. + * + * @param sourceType the descriptor of the source type + * @param targetType the descriptor of the type the source should be converted into + * @param classLoader the {@code ClassLoader} to use + * + * @since 6.0 + * @see Converter + */ +@API(status = EXPERIMENTAL, since = "6.0") +public record ConversionContext(TypeDescriptor sourceType, TypeDescriptor targetType, ClassLoader classLoader) { + + /** + * Create a new {@code ConversionContext}, expecting an instance of the + * source instead of its type descriptor. + * + * @param source the source instance; may be {@code null} + * @param targetType the descriptor of the type the source should be converted into + * @param classLoader the {@code ClassLoader} to use; may be {@code null} to + * use the default {@code ClassLoader} + */ + public ConversionContext(@Nullable Object source, TypeDescriptor targetType, @Nullable ClassLoader classLoader) { + this(TypeDescriptor.forInstance(source), targetType, + classLoader != null ? classLoader : getDefaultClassLoader()); + } + +} 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 34ed6ef458f4..68da9058ea8d 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,15 +10,16 @@ package org.junit.platform.commons.support.conversion; +import static org.apiguardian.api.API.Status.DEPRECATED; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.MAINTAINED; -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.jspecify.annotations.Nullable; -import org.junit.platform.commons.util.ClassLoaderUtils; /** * {@code ConversionSupport} provides static utility methods for converting a @@ -29,17 +30,6 @@ @API(status = MAINTAINED, since = "1.13.3") public final class ConversionSupport { - private static final List stringToObjectConverters = List.of( // - new StringToBooleanConverter(), // - new StringToCharacterConverter(), // - new StringToNumberConverter(), // - new StringToClassConverter(), // - new StringToEnumConverter(), // - new StringToJavaTimeConverter(), // - new StringToCommonJavaTypesConverter(), // - new FallbackStringToObjectConverter() // - ); - private ConversionSupport() { /* no-op */ } @@ -48,43 +38,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; @@ -96,49 +49,44 @@ private ConversionSupport() { * type is a reference type * * @since 1.11 + * @see DefaultConverter + * @deprecated Use {@link #convert(Object, ConversionContext)} instead. */ - @SuppressWarnings("unchecked") + @Deprecated + @API(status = DEPRECATED, since = "6.0") public static @Nullable T convert(@Nullable String source, Class targetType, @Nullable ClassLoader classLoader) { - if (source == null) { - if (targetType.isPrimitive()) { - throw new ConversionException( - "Cannot convert null to primitive value of type " + targetType.getTypeName()); - } - return null; - } - - if (String.class.equals(targetType)) { - return (T) source; - } + ConversionContext context = new ConversionContext(source, TypeDescriptor.forClass(targetType), classLoader); + return convert(source, context); + } - 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 conversionException) { - // simply rethrow it - throw conversionException; - } - // else - throw new ConversionException( - "Failed to convert String \"%s\" to type %s".formatted(source, targetType.getTypeName()), ex); - } - } + /** + * 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 context the context for the conversion + * @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 6.0 + */ + @API(status = EXPERIMENTAL, since = "6.0") + @SuppressWarnings({ "unchecked", "rawtypes", "TypeParameterUnusedInFormals" }) + public static @Nullable T convert(@Nullable Object source, ConversionContext context) { + ServiceLoader serviceLoader = ServiceLoader.load(Converter.class, context.classLoader()); - throw new ConversionException( - "No built-in converter for source type java.lang.String and target type " + targetType.getTypeName()); - } + Converter converter = Stream.concat( // + StreamSupport.stream(serviceLoader.spliterator(), false), // + Stream.of(DefaultConverter.INSTANCE)) // + .filter(candidate -> candidate.canConvert(context)) // + .findFirst() // + .orElseThrow(() -> new ConversionException( + "No registered or built-in converter for source '%s' and target type %s".formatted( // + source, context.targetType()))); - private static Class toWrapperType(Class targetType) { - Class wrapperType = getWrapperType(targetType); - return wrapperType != null ? wrapperType : targetType; + return (T) converter.convert(source, context); } } 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..74300e49a5a2 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/Converter.java @@ -0,0 +1,65 @@ +/* + * 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.jspecify.annotations.Nullable; + +/** + * {@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. + * + * @param the type of the source to convert + * @param the type the source should be converted into + * + * @since 6.0 + * @see ConversionSupport + * @see TypedConverter + */ +@API(status = EXPERIMENTAL, since = "6.0") +public interface Converter { + + /** + * Determine if the supplied conversion context is supported. + * + * @param context the context for the conversion; never {@code null} + * @return {@code true} if the conversion is supported + */ + boolean canConvert(ConversionContext context); + + /** + * Convert the supplied source object according to the supplied conversion context. + *

This method will only be invoked if {@link #canConvert(ConversionContext)} + * returned {@code true} for the same context. + * + * @param source the source object to convert; may be {@code null} + * but only if the target type is a reference type + * @param context the context for the conversion; 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 + */ + @Nullable + T convert(@Nullable S source, ConversionContext context) 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..7b6ec940c356 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/DefaultConverter.java @@ -0,0 +1,165 @@ +/* + * 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.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.jspecify.annotations.Nullable; +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 6.0 + */ +@API(status = INTERNAL, since = "6.0") +public class DefaultConverter implements Converter { + + static final DefaultConverter INSTANCE = new DefaultConverter(); + + private static final List> stringToObjectConverters = List.of( // + 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 conversion context is supported. + *

FIXME add more content from {@link Converter#convert} about the conversion algorithm + * + * @param context the context for the conversion; never {@code null} + * @return {@code true} if the conversion is supported + */ + @Override + public boolean canConvert(ConversionContext context) { + if (context.sourceType().equals(TypeDescriptor.NONE)) { + return !context.targetType().isPrimitive(); + } + + if (!String.class.equals(context.sourceType().getType())) { + return false; + } + + if (String.class.equals(context.targetType().getType())) { + return true; + } + + return stringToObjectConverters.stream().anyMatch(candidate -> candidate.canConvert(context)); + } + + /** + * Convert the supplied source {@link 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 {@link String} to convert; may be {@code null} + * but only if the target type is a reference type + * @param context the context for the conversion; 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 @Nullable Object convert(@Nullable String source, ConversionContext context) throws ConversionException { + if (source == null) { + if (context.targetType().isPrimitive()) { + throw new ConversionException("Cannot convert null to primitive value of type " + context.targetType()); + } + return null; + } + + if (String.class.equals(context.targetType().getType())) { + return source; + } + + Optional> converter = stringToObjectConverters.stream().filter( + candidate -> candidate.canConvert(context)).findFirst(); + if (converter.isPresent()) { + try { + return converter.get().convert(source, context); + } + catch (Exception ex) { + if (ex instanceof ConversionException conversionException) { + // simply rethrow it + throw conversionException; + } + // else + throw new ConversionException( + "Failed to convert String \"%s\" to type %s".formatted(source, context.targetType()), ex); + } + } + + throw new ConversionException( + "No built-in converter for source type java.lang.String and target type " + context.targetType()); + } + +} 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 916406e3fbcb..17ecdcfabfb3 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 @@ -30,7 +30,7 @@ import org.junit.platform.commons.util.Preconditions; /** - * {@code FallbackStringToObjectConverter} is a {@link StringToObjectConverter} + * {@code FallbackStringToObjectConverter} is a {@link StringToTargetTypeConverter} * that provides a fallback conversion strategy for converting from a * {@link String} to a given target type by invoking a static factory method * or factory constructor defined in the target type. @@ -52,7 +52,7 @@ * @since 1.11 * @see ConversionSupport */ -class FallbackStringToObjectConverter implements StringToObjectConverter { +class FallbackStringToObjectConverter extends StringToTargetTypeConverter { /** * Implementation of the NULL Object Pattern. @@ -71,12 +71,13 @@ class FallbackStringToObjectConverter implements StringToObjectConverter { = new ConcurrentHashMap<>(64); @Override - public boolean canConvertTo(Class targetType) { + boolean canConvert(Class targetType) { return findFactoryExecutable(targetType) != NULL_EXECUTABLE; } @Override - public @Nullable Object convert(String source, Class targetType) throws Exception { + @Nullable + Object convert(String source, Class targetType) { 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..57c0834c0624 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 StringToWrapperTypeConverter { @Override - public boolean canConvertTo(Class targetType) { + boolean canConvert(Class targetType) { return targetType == Boolean.class; } @Override - public Object convert(String source, Class targetType) { + Boolean convert(String source, Class targetType) throws ConversionException { 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..255e311534b3 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 StringToWrapperTypeConverter { @Override - public boolean canConvertTo(Class targetType) { + boolean canConvert(Class targetType) { return targetType == Character.class; } @Override - public Object convert(String source, Class targetType) { + Character convert(String source, Class targetType) throws ConversionException { 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 a2d5cbb9322e..4ef3b51b5629 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,23 +12,20 @@ import org.jspecify.annotations.Nullable; import org.junit.platform.commons.support.ReflectionSupport; +import org.junit.platform.commons.util.Preconditions; -class StringToClassConverter implements StringToObjectConverter { +class StringToClassConverter implements Converter> { @Override - public boolean canConvertTo(Class targetType) { - return targetType == Class.class; + public boolean canConvert(ConversionContext context) { + return !context.sourceType().equals(TypeDescriptor.NONE) && context.targetType().getType() == Class.class; } @Override - public Object convert(String source, Class targetType) throws Exception { - throw new UnsupportedOperationException("Invoke convert(String, Class, ClassLoader) instead"); - } - - @Override - public @Nullable Object convert(String className, Class targetType, ClassLoader classLoader) throws Exception { + public @Nullable Class convert(@Nullable String className, ConversionContext context) { + Preconditions.notNull(className, "className cannot be null"); // @formatter:off - return ReflectionSupport.tryToLoadClass(className, classLoader) + return ReflectionSupport.tryToLoadClass(className, context.classLoader()) .getOrThrow(cause -> new ConversionException( "Failed to convert String \"" + className + "\" to type java.lang.Class", cause)); // @formatter:on 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 36f1eee73ec3..555181e35c05 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 @@ -25,7 +25,7 @@ import org.junit.platform.commons.util.Preconditions; -class StringToCommonJavaTypesConverter implements StringToObjectConverter { +class StringToCommonJavaTypesConverter extends StringToTargetTypeConverter { private static final Map, Function> CONVERTERS = Map.of( // // java.io and java.nio @@ -42,12 +42,12 @@ class StringToCommonJavaTypesConverter implements StringToObjectConverter { ); @Override - public boolean canConvertTo(Class targetType) { + boolean canConvert(Class targetType) { return CONVERTERS.containsKey(targetType); } @Override - public Object convert(String source, Class targetType) throws Exception { + Object convert(String source, Class targetType) { Function converter = Preconditions.notNull(CONVERTERS.get(targetType), () -> "No registered converter for %s".formatted(targetType.getName())); return converter.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..8b712a77a7ec 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,17 @@ package org.junit.platform.commons.support.conversion; -class StringToEnumConverter implements StringToObjectConverter { +@SuppressWarnings("rawtypes") +class StringToEnumConverter extends StringToTargetTypeConverter { @Override - public boolean canConvertTo(Class targetType) { + boolean canConvert(Class targetType) { return targetType.isEnum(); } + @SuppressWarnings("unchecked") @Override - @SuppressWarnings({ "unchecked", "rawtypes" }) - public Object convert(String source, Class targetType) throws Exception { + Enum convert(String source, Class targetType) throws ConversionException { 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 c49bd2bc8bdf..fdd8ade56501 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 @@ -31,7 +31,7 @@ import org.junit.platform.commons.util.Preconditions; -class StringToJavaTimeConverter implements StringToObjectConverter { +class StringToJavaTimeConverter extends StringToTargetTypeConverter { private static final Map, Function> CONVERTERS = Map.ofEntries( // entry(Duration.class, Duration::parse), // @@ -51,12 +51,12 @@ class StringToJavaTimeConverter implements StringToObjectConverter { ); @Override - public boolean canConvertTo(Class targetType) { + boolean canConvert(Class targetType) { return CONVERTERS.containsKey(targetType); } @Override - public Object convert(String source, Class targetType) throws Exception { + Object convert(String source, Class targetType) throws ConversionException { Function converter = Preconditions.notNull(CONVERTERS.get(targetType), () -> "No registered converter for %s".formatted(targetType.getName())); return converter.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 f21ee0429c1b..6ce863972d76 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 @@ -17,9 +17,9 @@ import org.junit.platform.commons.util.Preconditions; -class StringToNumberConverter implements StringToObjectConverter { +class StringToNumberConverter extends StringToWrapperTypeConverter { - private static final Map, Function> CONVERTERS = Map.of( // + private static final Map, Function> CONVERTERS = Map.of( // Byte.class, Byte::decode, // Short.class, Short::decode, // Integer.class, Integer::decode, // @@ -34,13 +34,13 @@ class StringToNumberConverter implements StringToObjectConverter { ); @Override - public boolean canConvertTo(Class targetType) { + boolean canConvert(Class targetType) { return CONVERTERS.containsKey(targetType); } @Override - public Object convert(String source, Class targetType) { - Function converter = Preconditions.notNull(CONVERTERS.get(targetType), + Number convert(String source, Class targetType) throws ConversionException { + Function converter = Preconditions.notNull(CONVERTERS.get(targetType), () -> "No registered converter for %s".formatted(targetType.getName())); return converter.apply(source.replace("_", "")); } diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToTargetTypeConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToTargetTypeConverter.java new file mode 100644 index 000000000000..860e97a11342 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToTargetTypeConverter.java @@ -0,0 +1,47 @@ +/* + * 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 org.jspecify.annotations.Nullable; +import org.junit.platform.commons.util.Preconditions; + +/** + * Internal API for converting arguments of type {@link String} to a specified + * target type. + */ +abstract class StringToTargetTypeConverter implements Converter { + + @Override + public final boolean canConvert(ConversionContext context) { + return !context.sourceType().equals(TypeDescriptor.NONE) && canConvert(context.targetType().getType()); + } + + /** + * Determine if this converter can convert from a {@link String} to the + * supplied target type. + */ + abstract boolean canConvert(Class targetType); + + @Override + public final @Nullable T convert(@Nullable String source, ConversionContext context) { + Preconditions.notNull(source, "source cannot be null"); + return convert(source, context.targetType().getType()); + } + + /** + * Convert the supplied {@link String} to the supplied target type. + * + *

This method will only be invoked if {@link #canConvert(Class)} + * returned {@code true} for the same target type. + */ + abstract @Nullable T convert(String source, Class targetType); + +} 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/StringToWrapperTypeConverter.java similarity index 51% rename from junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToObjectConverter.java rename to junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToWrapperTypeConverter.java index 6c3bfffae363..fb3f4e266a6b 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/StringToWrapperTypeConverter.java @@ -11,45 +11,45 @@ package org.junit.platform.commons.support.conversion; import org.jspecify.annotations.Nullable; +import org.junit.platform.commons.util.Preconditions; /** * Internal API for converting arguments of type {@link String} to a specified - * target type. + * wrapper type. */ -interface StringToObjectConverter { +abstract class StringToWrapperTypeConverter implements Converter { + + @Override + public final boolean canConvert(ConversionContext context) { + return !context.sourceType().equals(TypeDescriptor.NONE) && canConvert(getTargetType(context)); + } /** * 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. - */ - @Nullable - Object convert(String source, Class targetType) throws Exception; + @Override + public final T convert(@Nullable String source, ConversionContext context) throws ConversionException { + Preconditions.notNull(source, "source cannot be null"); + return convert(source, getTargetType(context)); + } /** * 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 @Nullable Object convert(String source, Class targetType, ClassLoader classLoader) throws Exception { - return convert(source, targetType); + abstract T convert(String source, Class targetType) throws ConversionException; + + private static Class getTargetType(ConversionContext context) { + return context.targetType().getWrapperType() // + .orElseGet(() -> context.targetType().getType()); } } 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..6d9ef9333892 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/TypeDescriptor.java @@ -0,0 +1,113 @@ +/* + * 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 java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; + +import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.commons.util.ReflectionUtils; + +/** + * + * + * @since 6.0 + */ +@API(status = EXPERIMENTAL, since = "6.0") +public final class TypeDescriptor { + + /** + * {@code TypeDescriptor} returned when no value is available. + */ + public static final TypeDescriptor NONE = new TypeDescriptor(null); + + private final @Nullable Class type; + + public static TypeDescriptor forClass(Class clazz) { + Preconditions.notNull(clazz, () -> "clazz must not be null"); + return new TypeDescriptor(clazz); + } + + public static TypeDescriptor forInstance(@Nullable Object instance) { + return instance != null ? forClass(instance.getClass()) : NONE; + } + + public static TypeDescriptor forField(Field field) { + Preconditions.notNull(field, "field must not be null"); + return forClass(field.getType()); + } + + public static TypeDescriptor forParameter(Parameter parameter) { + Preconditions.notNull(parameter, "parameter must not be null"); + return forClass(parameter.getType()); + } + + private TypeDescriptor(@Nullable Class type) { + this.type = type; + } + + public Class getType() { + if (type == null) { + throw new NoSuchElementException("No type present"); + } + return type; + } + + /** + * Get the wrapper type of this type descriptor, if available. + * + *

If this type descriptor represents a primitive type, this method + * returns the corresponding wrapped type. Otherwise, this method returns + * {@link Optional#empty() empty()}. + * + * @return an {@code Optional} containing the wrapper type; never + * {@code null} but potentially empty + */ + // FIXME [NullAway] parameter type of referenced method is @NonNull, but parameter in functional interface method java.util.function.Function.apply(T) is @Nullable + @SuppressWarnings("NullAway") + public Optional> getWrapperType() { + return Optional.ofNullable(type).map(ReflectionUtils::getWrapperType); + } + + public boolean isPrimitive() { + return type != null && type.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 Objects.equals(this.type, that.type); + } + + @Override + public int hashCode() { + return Objects.hashCode(type); + } + + @Override + public String toString() { + return type != null ? type.getName() : "'null'"; + } + +} 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..eb48cdf76fe8 --- /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.jspecify.annotations.Nullable; +import org.junit.platform.commons.util.Preconditions; + +/** + * {@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 6.0 + */ +@API(status = EXPERIMENTAL, since = "6.0") +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(ConversionContext context) { + // FIXME add test cases with subtypes + return this.sourceType == context.sourceType().getType() && this.targetType == context.targetType().getType(); + } + + @Override + public final @Nullable T convert(@Nullable S source, ConversionContext context) { + return convert(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 @Nullable T convert(@Nullable S source) throws ConversionException; + +} 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 721d09d38952..731ce70a8b6b 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 @@ -25,9 +25,10 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.junit.platform.commons.support.ReflectionSupport; +import org.junit.platform.commons.support.conversion.ConversionContext; 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; /** * Unit tests for {@link DefaultArgumentConverter}. @@ -79,39 +80,33 @@ 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()); } @Test void delegatesStringsConversion() { - doReturn(null).when(underTest).convert(any(), any(), any(ClassLoader.class)); + doReturn(null).when(underTest).delegateConversion(any(), any()); convert("value", int.class); - verify(underTest).convert("value", int.class, getClassLoader(DefaultArgumentConverterTests.class)); + var context = new ConversionContext(TypeDescriptor.forClass(String.class), TypeDescriptor.forClass(int.class), + getClassLoader(getClass())); + verify(underTest).delegateConversion("value", context); } @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()); assertThatExceptionOfType(ArgumentConversionException.class) // .isThrownBy(() -> convert("value", int.class)) // .withCause(exception) // .withMessage(exception.getMessage()); - verify(underTest).convert("value", int.class, getClassLoader(DefaultArgumentConverterTests.class)); + var context = new ConversionContext(TypeDescriptor.forClass(String.class), TypeDescriptor.forClass(int.class), + getClassLoader(getClass())); + verify(underTest).delegateConversion("value", context); } @Test @@ -124,14 +119,16 @@ 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()); var clazz = (Class) convert(customTypeName, Class.class, testClassLoader); assertThat(clazz).isNotEqualTo(Enigma.class); assertThat(clazz).isNotNull().isEqualTo(customType); assertThat(clazz.getClassLoader()).isSameAs(testClassLoader); - verify(underTest).convert(customTypeName, Class.class, testClassLoader); + var context = new ConversionContext(TypeDescriptor.forClass(String.class), + TypeDescriptor.forClass(Class.class), testClassLoader); + verify(underTest).delegateConversion(customTypeName, context); } } @@ -144,15 +141,15 @@ private void assertConverts(@Nullable Object input, Class targetClass, @Nulla .describedAs(input + " --(" + targetClass.getName() + ")--> " + expectedOutput) // .isEqualTo(expectedOutput); - verify(underTest, never()).convert(any(), any(), any(ClassLoader.class)); + verify(underTest, never()).delegateConversion(any(), any()); } private @Nullable Object convert(@Nullable Object input, Class targetClass) { - return convert(input, targetClass, ClassLoaderUtils.getClassLoader(getClass())); + return convert(input, targetClass, getClassLoader(getClass())); } private @Nullable Object convert(@Nullable Object input, Class targetClass, ClassLoader classLoader) { - return underTest.convert(input, targetClass, classLoader); + return underTest.convert(input, TypeDescriptor.forClass(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..fb2ba359447a --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/converter/LocaleConverter.java @@ -0,0 +1,30 @@ +/* + * 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.jspecify.annotations.Nullable; +import org.junit.platform.commons.support.conversion.TypedConverter; + +// FIXME move to ConversionSupportIntegrationTests +public class LocaleConverter extends TypedConverter { + + public LocaleConverter() { + super(String.class, Locale.class); + } + + @Override + protected @Nullable Locale convert(@Nullable String source) { + return source != null ? Locale.forLanguageTag(source) : null; + } + +} 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 82% 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 4dc0fefb85e0..a87686331048 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 6.0 */ -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.forClass(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.forClass(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.forClass(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.forClass(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.forClass(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,11 @@ void convertsStringToClassWithCustomTypeFromDifferentClassLoader() throws Except var declaringExecutable = ReflectionSupport.findMethod(customType, "foo").orElseThrow(); assertThat(declaringExecutable.getDeclaringClass().getClassLoader()).isSameAs(testClassLoader); - var clazz = (Class) convert(customTypeName, Class.class, classLoader(declaringExecutable)); + var context = new ConversionContext(customTypeName, TypeDescriptor.forClass(Class.class), + classLoader(declaringExecutable)); + assertThat(canConvert(context)).isTrue(); + + var clazz = (Class) convert(customTypeName, context); assertThat(clazz).isNotNull().isNotEqualTo(Enigma.class).isEqualTo(customType); assertThat(clazz.getClassLoader()).isSameAs(testClassLoader); } @@ -308,23 +331,35 @@ void convertsStringToUUID() { // ------------------------------------------------------------------------- private void assertConverts(@Nullable String input, Class targetClass, @Nullable Object expectedOutput) { - var result = convert(input, targetClass); + TypeDescriptor typeDescriptor = TypeDescriptor.forClass(targetClass); + + assertThat(canConvert(input, typeDescriptor)).isTrue(); + + var result = convert(input, typeDescriptor); assertThat(result) // .describedAs(input + " --(" + targetClass.getName() + ")--> " + expectedOutput) // .isEqualTo(expectedOutput); } - private @Nullable Object convert(@Nullable String input, Class targetClass) { - return convert(input, targetClass, classLoader()); + private boolean canConvert(@Nullable String input, TypeDescriptor targetClass) { + return DefaultConverter.INSTANCE.canConvert(new ConversionContext(input, targetClass, classLoader())); + } + + private boolean canConvert(ConversionContext context) { + return DefaultConverter.INSTANCE.canConvert(context); + } + + private @Nullable Object convert(@Nullable String input, TypeDescriptor targetClass) { + return convert(input, new ConversionContext(input, targetClass, classLoader())); } - private @Nullable Object convert(@Nullable String input, Class targetClass, ClassLoader classLoader) { - return ConversionSupport.convert(input, targetClass, classLoader); + private @Nullable Object convert(@Nullable String input, ConversionContext context) { + return DefaultConverter.INSTANCE.convert(input, context); } private static ClassLoader classLoader() { - Method declaringExecutable = ReflectionSupport.findMethod(ConversionSupportTests.class, "foo").orElseThrow(); + Method declaringExecutable = ReflectionSupport.findMethod(DefaultConverterTests.class, "foo").orElseThrow(); 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 5bfc428ec8d7..cd8062afd606 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 @@ -87,13 +87,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,7 +120,7 @@ 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); diff --git a/platform-tests/src/test/java/org/junit/platform/commons/support/conversion/TypeDescriptorTests.java b/platform-tests/src/test/java/org/junit/platform/commons/support/conversion/TypeDescriptorTests.java new file mode 100644 index 000000000000..7fd62634e7ec --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/commons/support/conversion/TypeDescriptorTests.java @@ -0,0 +1,28 @@ +/* + * 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.junit.jupiter.api.EqualsAndHashCodeAssertions.assertEqualsAndHashCode; + +import org.junit.jupiter.api.Test; + +class TypeDescriptorTests { + + @Test + void equalsAndHashCode() { + var typeDescriptor1 = TypeDescriptor.forClass(String.class); + var typeDescriptor2 = TypeDescriptor.forClass(String.class); + var typeDescriptor3 = TypeDescriptor.forClass(Object.class); + + assertEqualsAndHashCode(typeDescriptor1, typeDescriptor2, typeDescriptor3); + } + +} 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 dcc0038d88fb..507838233793 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 @@ -13,6 +13,7 @@ requires kotlin.stdlib static requires kotlinx.coroutines.core static requires org.apiguardian.api static transitive requires org.jspecify 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.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.launcher org.junit.platform.reporting org.junit.platform.suite.api org.junit.platform.suite.engine org.junit.platform.testkit org.junit.vintage.engine From 2b180cdecbeb2df9c5a499f6303d79b065524605 Mon Sep 17 00:00:00 2001 From: Stefano Cordio Date: Tue, 22 Jul 2025 08:59:17 +0200 Subject: [PATCH 2/4] Replace `getOrThrow` with `getNonNullOrThrow` --- .../commons/support/conversion/ConversionException.java | 3 ++- .../commons/support/conversion/StringToClassConverter.java | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionException.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionException.java index d5fe3902f8fb..92cf75effb74 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionException.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionException.java @@ -15,6 +15,7 @@ import java.io.Serial; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.junit.platform.commons.JUnitException; /** @@ -33,7 +34,7 @@ public ConversionException(String message) { super(message); } - public ConversionException(String message, Throwable cause) { + public ConversionException(String message, @Nullable Throwable cause) { super(message, cause); } 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 4ef3b51b5629..23b81a8211e3 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 @@ -22,11 +22,11 @@ public boolean canConvert(ConversionContext context) { } @Override - public @Nullable Class convert(@Nullable String className, ConversionContext context) { + public Class convert(@Nullable String className, ConversionContext context) { Preconditions.notNull(className, "className cannot be null"); // @formatter:off return ReflectionSupport.tryToLoadClass(className, context.classLoader()) - .getOrThrow(cause -> new ConversionException( + .getNonNullOrThrow(cause -> new ConversionException( "Failed to convert String \"" + className + "\" to type java.lang.Class", cause)); // @formatter:on } From a57f5c9ba657f2f22f11aaae97493e09d74f7324 Mon Sep 17 00:00:00 2001 From: Stefano Cordio Date: Tue, 22 Jul 2025 12:21:42 +0200 Subject: [PATCH 3/4] Move nullability declaration to type parameter --- .../platform/commons/support/conversion/Converter.java | 3 +-- .../commons/support/conversion/DefaultConverter.java | 2 +- .../conversion/FallbackStringToObjectConverter.java | 2 +- .../support/conversion/StringToTargetTypeConverter.java | 6 +++--- .../commons/support/conversion/TypedConverter.java | 7 ++++--- .../junit/jupiter/params/converter/LocaleConverter.java | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) 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 index 74300e49a5a2..0e5bb855b382 100644 --- 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 @@ -37,7 +37,7 @@ * @see TypedConverter */ @API(status = EXPERIMENTAL, since = "6.0") -public interface Converter { +public interface Converter { /** * Determine if the supplied conversion context is supported. @@ -59,7 +59,6 @@ public interface Converter { * type is a reference type * @throws ConversionException if an error occurs during the conversion */ - @Nullable T convert(@Nullable S source, ConversionContext context) 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 index 7b6ec940c356..b886925b1739 100644 --- 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 @@ -44,7 +44,7 @@ * @since 6.0 */ @API(status = INTERNAL, since = "6.0") -public class DefaultConverter implements Converter { +public class DefaultConverter implements Converter { static final DefaultConverter INSTANCE = new DefaultConverter(); 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 17ecdcfabfb3..b682a6a70028 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 @@ -52,7 +52,7 @@ * @since 1.11 * @see ConversionSupport */ -class FallbackStringToObjectConverter extends StringToTargetTypeConverter { +class FallbackStringToObjectConverter extends StringToTargetTypeConverter<@Nullable Object> { /** * Implementation of the NULL Object Pattern. diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToTargetTypeConverter.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToTargetTypeConverter.java index 860e97a11342..7b056657f219 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToTargetTypeConverter.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/StringToTargetTypeConverter.java @@ -17,7 +17,7 @@ * Internal API for converting arguments of type {@link String} to a specified * target type. */ -abstract class StringToTargetTypeConverter implements Converter { +abstract class StringToTargetTypeConverter implements Converter { @Override public final boolean canConvert(ConversionContext context) { @@ -31,7 +31,7 @@ public final boolean canConvert(ConversionContext context) { abstract boolean canConvert(Class targetType); @Override - public final @Nullable T convert(@Nullable String source, ConversionContext context) { + public final T convert(@Nullable String source, ConversionContext context) { Preconditions.notNull(source, "source cannot be null"); return convert(source, context.targetType().getType()); } @@ -42,6 +42,6 @@ public final boolean canConvert(ConversionContext context) { *

This method will only be invoked if {@link #canConvert(Class)} * returned {@code true} for the same target type. */ - abstract @Nullable T convert(String source, Class targetType); + abstract T convert(String source, Class targetType); } 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 index eb48cdf76fe8..5e63f73e44e5 100644 --- 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 @@ -26,7 +26,7 @@ * @since 6.0 */ @API(status = EXPERIMENTAL, since = "6.0") -public abstract class TypedConverter implements Converter { +public abstract class TypedConverter implements Converter { private final Class sourceType; private final Class targetType; @@ -45,12 +45,13 @@ protected TypedConverter(Class sourceType, Class targetType) { @Override public final boolean canConvert(ConversionContext context) { + // FIXME TypeDescriptor.NONE handling? // FIXME add test cases with subtypes return this.sourceType == context.sourceType().getType() && this.targetType == context.targetType().getType(); } @Override - public final @Nullable T convert(@Nullable S source, ConversionContext context) { + public final T convert(@Nullable S source, ConversionContext context) { return convert(source); } @@ -63,6 +64,6 @@ public final boolean canConvert(ConversionContext context) { * type is a reference type * @throws ConversionException if an error occurs during the conversion */ - protected abstract @Nullable T convert(@Nullable S source) throws ConversionException; + protected abstract T convert(@Nullable S source) throws ConversionException; } 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 index fb2ba359447a..023712c8c930 100644 --- 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 @@ -16,7 +16,7 @@ import org.junit.platform.commons.support.conversion.TypedConverter; // FIXME move to ConversionSupportIntegrationTests -public class LocaleConverter extends TypedConverter { +public class LocaleConverter extends TypedConverter { public LocaleConverter() { super(String.class, Locale.class); From 7903c2b37635578adc4575e9949b4394173d22db Mon Sep 17 00:00:00 2001 From: Stefano Cordio Date: Wed, 23 Jul 2025 00:13:45 +0200 Subject: [PATCH 4/4] Rename `TypedConverter` to `SimpleConverter`, enforce non-null source --- .../commons/support/conversion/Converter.java | 4 ++-- ...pedConverter.java => SimpleConverter.java} | 24 ++++++++++--------- .../params/converter/LocaleConverter.java | 9 ++++--- 3 files changed, 19 insertions(+), 18 deletions(-) rename junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/{TypedConverter.java => SimpleConverter.java} (68%) 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 index 0e5bb855b382..f30b731ceb45 100644 --- 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 @@ -25,7 +25,7 @@ * instances may potentially be cached and called from different threads, they * should be thread-safe. * - *

Extend {@link TypedConverter} if your implementation always converts + *

Extend {@link SimpleConverter} 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. * @@ -34,7 +34,7 @@ * * @since 6.0 * @see ConversionSupport - * @see TypedConverter + * @see SimpleConverter */ @API(status = EXPERIMENTAL, since = "6.0") public interface Converter { 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/SimpleConverter.java similarity index 68% rename from junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/TypedConverter.java rename to junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/SimpleConverter.java index 5e63f73e44e5..68e888ae4f0e 100644 --- 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/SimpleConverter.java @@ -17,41 +17,43 @@ import org.junit.platform.commons.util.Preconditions; /** - * {@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. + * {@code SimpleConverter} 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 6.0 */ @API(status = EXPERIMENTAL, since = "6.0") -public abstract class TypedConverter implements Converter { +public abstract class SimpleConverter implements Converter { private final Class sourceType; private final Class targetType; /** - * Create a new {@code TypedConversionService}. + * Create a new {@code SimpleConverter}. * * @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) { + protected SimpleConverter(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(ConversionContext context) { - // FIXME TypeDescriptor.NONE handling? - // FIXME add test cases with subtypes - return this.sourceType == context.sourceType().getType() && this.targetType == context.targetType().getType(); + // FIXME adjust for subtypes + return !context.sourceType().equals(TypeDescriptor.NONE) // + && this.sourceType == context.sourceType().getType() // + && this.targetType == context.targetType().getType(); } @Override public final T convert(@Nullable S source, ConversionContext context) { + Preconditions.notNull(source, "source cannot be null"); return convert(source); } @@ -59,11 +61,11 @@ public final T convert(@Nullable S source, ConversionContext context) { * 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} + * @param source the source object to convert; 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 */ - protected abstract T convert(@Nullable S source) throws ConversionException; + protected abstract T convert(S source) throws ConversionException; } 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 index 023712c8c930..480de1013db1 100644 --- 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 @@ -12,19 +12,18 @@ import java.util.Locale; -import org.jspecify.annotations.Nullable; -import org.junit.platform.commons.support.conversion.TypedConverter; +import org.junit.platform.commons.support.conversion.SimpleConverter; // FIXME move to ConversionSupportIntegrationTests -public class LocaleConverter extends TypedConverter { +public class LocaleConverter extends SimpleConverter { public LocaleConverter() { super(String.class, Locale.class); } @Override - protected @Nullable Locale convert(@Nullable String source) { - return source != null ? Locale.forLanguageTag(source) : null; + protected Locale convert(String source) { + return Locale.forLanguageTag(source); } }