From 09f49d2c862219e731858f2bb7fe418e32b44f0b Mon Sep 17 00:00:00 2001 From: Stefano Cordio Date: Sat, 19 Apr 2025 13:58:43 +0200 Subject: [PATCH 1/7] Add `Locale` conversion format configuration property --- .../ParameterizedInvocationContext.java | 2 +- .../junit/jupiter/params/ResolverFacade.java | 8 ++-- .../aggregator/DefaultArgumentsAccessor.java | 6 ++- .../converter/DefaultArgumentConverter.java | 18 +++++++-- .../converter/LocaleConversionFormat.java | 39 +++++++++++++++++++ .../DefaultArgumentsAccessorTests.java | 5 ++- .../DefaultArgumentConverterTests.java | 5 ++- .../ArgumentsAccessorKotlinTests.kt | 5 ++- 8 files changed, 76 insertions(+), 12 deletions(-) create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/LocaleConversionFormat.java diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContext.java index 207817c69ebb..33964ced7b50 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContext.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContext.java @@ -72,7 +72,7 @@ private void storeParameterInfo(ExtensionContext context) { ParameterDeclarations declarations = this.declarationContext.getResolverFacade().getIndexedParameterDeclarations(); ClassLoader classLoader = getClassLoader(this.declarationContext.getTestClass()); Object[] arguments = this.arguments.getConsumedPayloads(); - ArgumentsAccessor accessor = DefaultArgumentsAccessor.create(invocationIndex, classLoader, arguments); + ArgumentsAccessor accessor = DefaultArgumentsAccessor.create(context, invocationIndex, classLoader, arguments); new DefaultParameterInfo(declarations, accessor).store(context); } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java index 28eda4b8186a..b2841d85f844 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java @@ -425,7 +425,7 @@ private static Converter createConverter(ParameterDeclaration declaration, Exten .map(clazz -> ParameterizedTestSpiInstantiator.instantiate(ArgumentConverter.class, clazz, extensionContext)) .map(converter -> AnnotationConsumerInitializer.initialize(declaration.getAnnotatedElement(), converter)) .map(Converter::new) - .orElse(Converter.DEFAULT); + .orElse(Converter.defaultConverter(extensionContext)); } // @formatter:on catch (Exception ex) { throw parameterResolutionException("Error creating ArgumentConverter", ex, declaration.getParameterIndex()); @@ -467,10 +467,12 @@ Object resolve(FieldContext fieldContext, ExtensionContext extensionContext, Eva private static class Converter implements Resolver { - private static final Converter DEFAULT = new Converter(DefaultArgumentConverter.INSTANCE); - private final ArgumentConverter argumentConverter; + private static Converter defaultConverter(ExtensionContext context) { + return new Converter(new DefaultArgumentConverter(context)); + } + Converter(ArgumentConverter argumentConverter) { this.argumentConverter = argumentConverter; } 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 811a8abd0518..40bf7213e1c7 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 @@ -19,6 +19,7 @@ import java.util.function.BiFunction; import org.apiguardian.api.API; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.converter.DefaultArgumentConverter; import org.junit.platform.commons.util.ClassUtils; import org.junit.platform.commons.util.Preconditions; @@ -40,10 +41,11 @@ public class DefaultArgumentsAccessor implements ArgumentsAccessor { private final Object[] arguments; private final BiFunction, Object> converter; - public static DefaultArgumentsAccessor create(int invocationIndex, ClassLoader classLoader, Object[] arguments) { + public static DefaultArgumentsAccessor create(ExtensionContext context, int invocationIndex, + ClassLoader classLoader, Object[] arguments) { Preconditions.notNull(classLoader, "ClassLoader must not be null"); - BiFunction, Object> converter = (source, targetType) -> DefaultArgumentConverter.INSTANCE // + BiFunction, Object> converter = (source, targetType) -> new DefaultArgumentConverter(context) // .convert(source, 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 8544019c1894..f612b5dcb06c 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 @@ -21,8 +21,10 @@ import java.util.Currency; import java.util.Locale; import java.util.UUID; +import java.util.function.Function; import org.apiguardian.api.API; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.params.support.FieldContext; import org.junit.platform.commons.support.conversion.ConversionException; @@ -50,10 +52,16 @@ @API(status = INTERNAL, since = "5.0") public class DefaultArgumentConverter implements ArgumentConverter { - public static final DefaultArgumentConverter INSTANCE = new DefaultArgumentConverter(); + private static final String DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME = "junit.jupiter.params.arguments.conversion.locale.format"; - private DefaultArgumentConverter() { - // nothing to initialize + private static final Function CONFIGURATION_TRANSFORMER = value -> LocaleConversionFormat.valueOf( + value.trim().toUpperCase(Locale.ROOT)); + + private final LocaleConversionFormat format; + + public DefaultArgumentConverter(ExtensionContext context) { + this.format = context.getConfigurationParameter(DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME, + CONFIGURATION_TRANSFORMER).orElse(LocaleConversionFormat.BCP_47); } @Override @@ -84,6 +92,10 @@ public final Object convert(Object source, Class targetType, ClassLoader clas } if (source instanceof String) { + if (targetType == Locale.class && format == LocaleConversionFormat.BCP_47) { + return Locale.forLanguageTag((String) source); + } + try { return convert((String) source, targetType, classLoader); } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/LocaleConversionFormat.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/LocaleConversionFormat.java new file mode 100644 index 000000000000..be37242f9a43 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/LocaleConversionFormat.java @@ -0,0 +1,39 @@ +/* + * 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 static org.apiguardian.api.API.Status.INTERNAL; + +import org.apiguardian.api.API; + +/** + * Enumeration of {@link java.util.Locale} conversion formats. + * + * @since 5.13 + */ +@API(status = INTERNAL, since = "5.13") +public enum LocaleConversionFormat { + + /** + * The ISO 639 alpha-2 or alpha-3 language code format. + * + * @see java.util.Locale#Locale(String) + */ + ISO_639, + + /** + * The IETF BCP 47 language tag format. + * + * @see java.util.Locale#forLanguageTag(String) + */ + BCP_47 + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessorTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessorTests.java index 792bae865f48..b5f6e941bc6e 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessorTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessorTests.java @@ -16,10 +16,12 @@ import static org.junit.jupiter.api.Assertions.assertIterableEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; import java.util.Arrays; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.platform.commons.PreconditionViolationException; /** @@ -164,8 +166,9 @@ void size() { } private static DefaultArgumentsAccessor defaultArgumentsAccessor(int invocationIndex, Object... arguments) { + var context = mock(ExtensionContext.class); var classLoader = DefaultArgumentsAccessorTests.class.getClassLoader(); - return DefaultArgumentsAccessor.create(invocationIndex, classLoader, arguments); + return DefaultArgumentsAccessor.create(context, invocationIndex, classLoader, arguments); } @SuppressWarnings("unused") 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 5336690e1c1e..ac66a0649d28 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 @@ -16,11 +16,13 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.junit.platform.commons.support.ReflectionSupport; @@ -35,7 +37,8 @@ */ class DefaultArgumentConverterTests { - private final DefaultArgumentConverter underTest = spy(DefaultArgumentConverter.INSTANCE); + private final ExtensionContext context = mock(); + private final DefaultArgumentConverter underTest = spy(new DefaultArgumentConverter(context)); @Test void isAwareOfNull() { diff --git a/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/ArgumentsAccessorKotlinTests.kt b/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/ArgumentsAccessorKotlinTests.kt index eb1aa43ed5fb..cbd6ca3a787b 100644 --- a/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/ArgumentsAccessorKotlinTests.kt +++ b/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/ArgumentsAccessorKotlinTests.kt @@ -13,6 +13,8 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtensionContext +import org.mockito.Mockito.mock /** * Unit tests for using [ArgumentsAccessor] from Kotlin. @@ -52,8 +54,9 @@ class ArgumentsAccessorKotlinTests { invocationIndex: Int, vararg arguments: Any ): DefaultArgumentsAccessor { + val context = mock(ExtensionContext::class.java) val classLoader = ArgumentsAccessorKotlinTests::class.java.classLoader - return DefaultArgumentsAccessor.create(invocationIndex, classLoader, arguments) + return DefaultArgumentsAccessor.create(context, invocationIndex, classLoader, arguments) } fun foo() { From 0e0e50c9fc34b4e2c62eae60fea499c18b3c0dee Mon Sep 17 00:00:00 2001 From: Stefano Cordio Date: Sat, 19 Apr 2025 23:46:36 +0200 Subject: [PATCH 2/7] Add `DefaultArgumentConverter` tests --- .../converter/DefaultArgumentConverter.java | 39 ++++++++++++++++--- .../converter/LocaleConversionFormat.java | 39 ------------------- .../DefaultArgumentConverterTests.java | 38 ++++++++++++++++++ 3 files changed, 71 insertions(+), 45 deletions(-) delete mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/LocaleConversionFormat.java 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 f612b5dcb06c..d75db63f7df1 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 @@ -52,16 +52,15 @@ @API(status = INTERNAL, since = "5.0") public class DefaultArgumentConverter implements ArgumentConverter { - private static final String DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME = "junit.jupiter.params.arguments.conversion.locale.format"; + static final String DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME = "junit.jupiter.params.arguments.conversion.locale.format"; - private static final Function CONFIGURATION_TRANSFORMER = value -> LocaleConversionFormat.valueOf( + private static final Function TRANSFORMER = value -> LocaleConversionFormat.valueOf( value.trim().toUpperCase(Locale.ROOT)); - private final LocaleConversionFormat format; + private final ExtensionContext context; public DefaultArgumentConverter(ExtensionContext context) { - this.format = context.getConfigurationParameter(DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME, - CONFIGURATION_TRANSFORMER).orElse(LocaleConversionFormat.BCP_47); + this.context = context; } @Override @@ -92,7 +91,7 @@ public final Object convert(Object source, Class targetType, ClassLoader clas } if (source instanceof String) { - if (targetType == Locale.class && format == LocaleConversionFormat.BCP_47) { + if (targetType == Locale.class && getLocaleConversionFormat() == LocaleConversionFormat.BCP_47) { return Locale.forLanguageTag((String) source); } @@ -109,8 +108,36 @@ public final Object convert(Object source, Class targetType, ClassLoader clas source.getClass().getTypeName(), targetType.getTypeName())); } + private LocaleConversionFormat getLocaleConversionFormat() { + return context.getConfigurationParameter(DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME, TRANSFORMER).orElse( + LocaleConversionFormat.BCP_47); + } + Object convert(String source, Class targetType, ClassLoader classLoader) { return ConversionSupport.convert(source, targetType, classLoader); } + /** + * Enumeration of {@link Locale} conversion formats. + * + * @since 5.13 + */ + enum LocaleConversionFormat { + + /** + * The IETF BCP 47 language tag format. + * + * @see Locale#forLanguageTag(String) + */ + BCP_47, + + /** + * The ISO 639 alpha-2 or alpha-3 language code format. + * + * @see Locale#Locale(String) + */ + ISO_639 + + } + } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/LocaleConversionFormat.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/LocaleConversionFormat.java deleted file mode 100644 index be37242f9a43..000000000000 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/LocaleConversionFormat.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 static org.apiguardian.api.API.Status.INTERNAL; - -import org.apiguardian.api.API; - -/** - * Enumeration of {@link java.util.Locale} conversion formats. - * - * @since 5.13 - */ -@API(status = INTERNAL, since = "5.13") -public enum LocaleConversionFormat { - - /** - * The ISO 639 alpha-2 or alpha-3 language code format. - * - * @see java.util.Locale#Locale(String) - */ - ISO_639, - - /** - * The IETF BCP 47 language tag format. - * - * @see java.util.Locale#forLanguageTag(String) - */ - BCP_47 - -} 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 ac66a0649d28..501f4c09a40c 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 @@ -12,14 +12,22 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.jupiter.params.converter.DefaultArgumentConverter.DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME; +import static org.junit.jupiter.params.converter.DefaultArgumentConverter.LocaleConversionFormat.BCP_47; +import static org.junit.jupiter.params.converter.DefaultArgumentConverter.LocaleConversionFormat.ISO_639; import static org.junit.platform.commons.util.ClassLoaderUtils.getClassLoader; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Locale; +import java.util.Optional; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; @@ -103,6 +111,36 @@ void delegatesStringsConversion() { verify(underTest).convert("value", int.class, getClassLoader(DefaultArgumentConverterTests.class)); } + @Test + void convertsLocaleWithDefaultFormat() { + when(context.getConfigurationParameter(eq(DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME), any())) // + .thenReturn(Optional.empty()); + + assertConverts("en", Locale.class, Locale.ENGLISH); + assertConverts("en-US", Locale.class, Locale.US); + } + + @Test + void convertsLocaleWithExplicitBcp47Format() { + when(context.getConfigurationParameter(eq(DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME), any())) // + .thenReturn(Optional.of(BCP_47)); + + assertConverts("en", Locale.class, Locale.ENGLISH); + assertConverts("en-US", Locale.class, Locale.US); + } + + @Test + void delegatesLocaleConversionWithExplicitIso639Format() { + when(context.getConfigurationParameter(eq(DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME), any())) // + .thenReturn(Optional.of(ISO_639)); + + doReturn(null).when(underTest).convert(any(), any(), any(ClassLoader.class)); + + convert("en", Locale.class); + + verify(underTest).convert("en", Locale.class, getClassLoader(DefaultArgumentConverterTests.class)); + } + @Test void throwsExceptionForDelegatedConversionFailure() { ConversionException exception = new ConversionException("fail"); From 3a621c00bef41b84e90cbbc06098de5083f65121 Mon Sep 17 00:00:00 2001 From: Stefano Cordio Date: Sun, 20 Apr 2025 09:49:48 +0200 Subject: [PATCH 3/7] Add `ParameterizedTest` integration tests --- .../converter/DefaultArgumentConverter.java | 2 +- .../ParameterizedTestIntegrationTests.java | 50 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) 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 d75db63f7df1..6ba5215c6341 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 @@ -52,7 +52,7 @@ @API(status = INTERNAL, since = "5.0") public class DefaultArgumentConverter implements ArgumentConverter { - static final String DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME = "junit.jupiter.params.arguments.conversion.locale.format"; + public static final String DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME = "junit.jupiter.params.arguments.conversion.locale.format"; private static final Function TRANSFORMER = value -> LocaleConversionFormat.valueOf( value.trim().toUpperCase(Locale.ROOT)); diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java index 0c0c44842427..58ae0dd33a73 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java @@ -22,6 +22,7 @@ import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.appendTestTemplateInvocationSegment; import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.uniqueIdForTestTemplateMethod; +import static org.junit.jupiter.params.converter.DefaultArgumentConverter.DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME; import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectIteration; @@ -476,6 +477,29 @@ void failsWhenNoArgumentsSourceIsDeclared() { "Configuration error: You must configure at least one arguments source for this @ParameterizedTest")))); } + @Test + void executesWithDefaultLocaleConversionFormat() { + var results = execute(LocaleConversionTestCase.class, "testWithBcp47", Locale.class); + + results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4)); + } + + @Test + void executesWithBcp47LocaleConversionFormat() { + var results = execute(Map.of(DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME, "bcp_47"), + LocaleConversionTestCase.class, "testWithBcp47", Locale.class); + + results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4)); + } + + @Test + void executesWithIso639LocaleConversionFormat() { + var results = execute(Map.of(DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME, "iso_639"), + LocaleConversionTestCase.class, "testWithIso639", Locale.class); + + results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4)); + } + private EngineExecutionResults execute(DiscoverySelector... selectors) { return EngineTestKit.engine(new JupiterTestEngine()).selectors(selectors).execute(); } @@ -484,6 +508,14 @@ private EngineExecutionResults execute(Class testClass, String methodName, Cl return execute(selectMethod(testClass, methodName, ClassUtils.nullSafeToString(methodParameterTypes))); } + private EngineExecutionResults execute(Map configurationParameters, Class testClass, + String methodName, Class... methodParameterTypes) { + return EngineTestKit.engine(new JupiterTestEngine()) // + .selectors(selectMethod(testClass, methodName, ClassUtils.nullSafeToString(methodParameterTypes))) // + .configurationParameters(configurationParameters) // + .execute(); + } + private EngineExecutionResults execute(String methodName, Class... methodParameterTypes) { return execute(TestCase.class, methodName, methodParameterTypes); } @@ -2508,6 +2540,24 @@ public static Stream zeroArgumentsProvider() { } } + static class LocaleConversionTestCase { + + @ParameterizedTest + @ValueSource(strings = "en-US") + void testWithBcp47(Locale locale) { + assertEquals("en", locale.getLanguage()); + assertEquals("US", locale.getCountry()); + } + + @ParameterizedTest + @ValueSource(strings = "en-US") + void testWithIso639(Locale locale) { + assertEquals("en-us", locale.getLanguage()); + assertEquals("", locale.getCountry()); + } + + } + private static class TwoSingleStringArgumentsProvider implements ArgumentsProvider { @Override From 31c59c64b6c9935bfba43553572c077571351f51 Mon Sep 17 00:00:00 2001 From: Stefano Cordio Date: Sun, 20 Apr 2025 19:05:59 +0200 Subject: [PATCH 4/7] Add release notes --- .../docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc index 0c9907c5208d..3aa59a63af04 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc @@ -98,7 +98,10 @@ to start reporting discovery issues. - Blank `@SentenceFragment` declarations - `@BeforeParameterizedClassInvocation` and `@AfterParameterizedClassInvocation` methods declared in non-parameterized test classes - +* New `junit.jupiter.params.arguments.conversion.locale.format` configuration parameter to select the format used for the conversion of `@ParameterizedTest` `Locale` arguments. +See the +<<../user-guide/index.adoc#writing-tests-parameterized-tests-argument-conversion-implicit, User Guide>> +for details. [[release-notes-5.13.0-M3-junit-vintage]] === JUnit Vintage From 4aeb5ff10333722a447c206d5a64594fb72a0366 Mon Sep 17 00:00:00 2001 From: Stefano Cordio Date: Sun, 20 Apr 2025 19:36:59 +0200 Subject: [PATCH 5/7] Update user guide --- .../release-notes-5.13.0-M3.adoc | 2 +- .../asciidoc/user-guide/writing-tests.adoc | 8 +++-- .../converter/DefaultArgumentConverter.java | 31 ++++++++++--------- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc index 3aa59a63af04..28a66dbe277d 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc @@ -98,7 +98,7 @@ to start reporting discovery issues. - Blank `@SentenceFragment` declarations - `@BeforeParameterizedClassInvocation` and `@AfterParameterizedClassInvocation` methods declared in non-parameterized test classes -* New `junit.jupiter.params.arguments.conversion.locale.format` configuration parameter to select the format used for the conversion of `@ParameterizedTest` `Locale` arguments. +* `java.util.Locale` arguments are now converted following the IETF BCP 47 language tag format. See the <<../user-guide/index.adoc#writing-tests-parameterized-tests-argument-conversion-implicit, User Guide>> for details. diff --git a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc index f413644bf869..90b5558ae5a6 100644 --- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc @@ -2483,10 +2483,14 @@ integral types: `byte`, `short`, `int`, `long`, and their boxed counterparts. | `java.time.ZoneId` | `"Europe/Berlin"` -> `ZoneId.of("Europe/Berlin")` | `java.time.ZoneOffset` | `"+02:30"` -> `ZoneOffset.ofHoursMinutes(2, 30)` | `java.util.Currency` | `"JPY"` -> `Currency.getInstance("JPY")` -| `java.util.Locale` | `"en"` -> `new Locale("en")` +| `java.util.Locale` | `"en-US"` -> `Locale.forLanguageTag("en-US")` | `java.util.UUID` | `"d043e930-7b3b-48e3-bdbe-5a3ccfb833db"` -> `UUID.fromString("d043e930-7b3b-48e3-bdbe-5a3ccfb833db")` |=== +WARNING: To revert to the old `java.util.Locale` conversion behavior, you can set the +`junit.jupiter.params.arguments.conversion.locale.format` configuration parameter to `iso_639`. +However, please note that this option is deprecated and will be removed in a future release. + [[writing-tests-parameterized-tests-argument-conversion-implicit-fallback]] ====== Fallback String-to-Object Conversion @@ -2523,7 +2527,7 @@ include::{testDir}/example/ParameterizedTestDemo.java[tags=implicit_fallback_con [[writing-tests-parameterized-tests-argument-conversion-explicit]] ===== Explicit Conversion -Instead of relying on implicit argument conversion you may explicitly specify an +Instead of relying on implicit argument conversion, you may explicitly specify an `ArgumentConverter` to use for a certain parameter using the `@ConvertWith` annotation like in the following example. Note that an implementation of `ArgumentConverter` must be declared as either a top-level class or as a `static` nested class. 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 6ba5215c6341..fbe4073ece66 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 @@ -52,6 +52,22 @@ @API(status = INTERNAL, since = "5.0") public class DefaultArgumentConverter implements ArgumentConverter { + /** + * Property name used to set the format for the conversion of {@link Locale} + * arguments: {@value} + * + *

Supported Values

+ *
    + *
  • {@code bcp_47}: uses the IETF BCP 47 language tag format, via + * {@link Locale#forLanguageTag(String)}
  • + *
  • {@code iso_639}: uses the ISO 639 alpha-2 or alpha-3 + * language code format, via {@link Locale#Locale(String)}
  • + *
+ * + *

If not specified, the default is {@code bcp_47}. + * + * @since 5.13 + */ public static final String DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME = "junit.jupiter.params.arguments.conversion.locale.format"; private static final Function TRANSFORMER = value -> LocaleConversionFormat.valueOf( @@ -117,25 +133,10 @@ Object convert(String source, Class targetType, ClassLoader classLoader) { return ConversionSupport.convert(source, targetType, classLoader); } - /** - * Enumeration of {@link Locale} conversion formats. - * - * @since 5.13 - */ enum LocaleConversionFormat { - /** - * The IETF BCP 47 language tag format. - * - * @see Locale#forLanguageTag(String) - */ BCP_47, - /** - * The ISO 639 alpha-2 or alpha-3 language code format. - * - * @see Locale#Locale(String) - */ ISO_639 } From 3c3e3f08ba79e89dd647f0107eb54fae89f64a93 Mon Sep 17 00:00:00 2001 From: Stefano Cordio Date: Mon, 21 Apr 2025 10:24:41 +0200 Subject: [PATCH 6/7] Cosmetic --- .../asciidoc/release-notes/release-notes-5.13.0-M3.adoc | 2 +- .../params/converter/DefaultArgumentConverter.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc index 28a66dbe277d..7ef1b11ae421 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc @@ -98,7 +98,7 @@ to start reporting discovery issues. - Blank `@SentenceFragment` declarations - `@BeforeParameterizedClassInvocation` and `@AfterParameterizedClassInvocation` methods declared in non-parameterized test classes -* `java.util.Locale` arguments are now converted following the IETF BCP 47 language tag format. +* `java.util.Locale` arguments are now converted according to the IETF BCP 47 language tag format. See the <<../user-guide/index.adoc#writing-tests-parameterized-tests-argument-conversion-implicit, User Guide>> for details. 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 fbe4073ece66..7b858cacf13b 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 @@ -58,10 +58,10 @@ public class DefaultArgumentConverter implements ArgumentConverter { * *

Supported Values

*
    - *
  • {@code bcp_47}: uses the IETF BCP 47 language tag format, via - * {@link Locale#forLanguageTag(String)}
  • - *
  • {@code iso_639}: uses the ISO 639 alpha-2 or alpha-3 - * language code format, via {@link Locale#Locale(String)}
  • + *
  • {@code bcp_47}: uses the IETF BCP 47 language tag format, delegating + * the conversion to {@link Locale#forLanguageTag(String)}
  • + *
  • {@code iso_639}: uses the ISO 639 alpha-2 or alpha-3 language code + * format, delegating the conversion to {@link Locale#Locale(String)}
  • *
* *

If not specified, the default is {@code bcp_47}. From a2f237597e248f10f42f20b28669a837c70d5353 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Mon, 28 Apr 2025 09:24:40 +0200 Subject: [PATCH 7/7] Polishing --- .../asciidoc/release-notes/release-notes-5.13.0-M3.adoc | 8 ++++---- .../src/docs/asciidoc/user-guide/writing-tests.adoc | 8 +++++--- .../java/org/junit/jupiter/params/ResolverFacade.java | 4 ++-- .../params/converter/DefaultArgumentConverter.java | 4 ++-- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc index 7ef1b11ae421..f67c3702c006 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc @@ -98,10 +98,10 @@ to start reporting discovery issues. - Blank `@SentenceFragment` declarations - `@BeforeParameterizedClassInvocation` and `@AfterParameterizedClassInvocation` methods declared in non-parameterized test classes -* `java.util.Locale` arguments are now converted according to the IETF BCP 47 language tag format. -See the -<<../user-guide/index.adoc#writing-tests-parameterized-tests-argument-conversion-implicit, User Guide>> -for details. +* `java.util.Locale` arguments are now converted according to the IETF BCP 47 language tag + format. See the + <<../user-guide/index.adoc#writing-tests-parameterized-tests-argument-conversion-implicit, User Guide>> + for details. [[release-notes-5.13.0-M3-junit-vintage]] === JUnit Vintage diff --git a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc index 90b5558ae5a6..e84ed85c59be 100644 --- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc @@ -2487,9 +2487,11 @@ integral types: `byte`, `short`, `int`, `long`, and their boxed counterparts. | `java.util.UUID` | `"d043e930-7b3b-48e3-bdbe-5a3ccfb833db"` -> `UUID.fromString("d043e930-7b3b-48e3-bdbe-5a3ccfb833db")` |=== -WARNING: To revert to the old `java.util.Locale` conversion behavior, you can set the -`junit.jupiter.params.arguments.conversion.locale.format` configuration parameter to `iso_639`. -However, please note that this option is deprecated and will be removed in a future release. +WARNING: To revert to the old `java.util.Locale` conversion behavior of version 5.12 and +earlier (which called the deprecated `Locale(String)` constructor), you can set the +`junit.jupiter.params.arguments.conversion.locale.format` +<> to `iso_639`. However, please +note that this parameter is deprecated and will be removed in a future release. [[writing-tests-parameterized-tests-argument-conversion-implicit-fallback]] ====== Fallback String-to-Object Conversion diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java index b2841d85f844..a1cad8e98419 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java @@ -425,7 +425,7 @@ private static Converter createConverter(ParameterDeclaration declaration, Exten .map(clazz -> ParameterizedTestSpiInstantiator.instantiate(ArgumentConverter.class, clazz, extensionContext)) .map(converter -> AnnotationConsumerInitializer.initialize(declaration.getAnnotatedElement(), converter)) .map(Converter::new) - .orElse(Converter.defaultConverter(extensionContext)); + .orElseGet(() -> Converter.createDefault(extensionContext)); } // @formatter:on catch (Exception ex) { throw parameterResolutionException("Error creating ArgumentConverter", ex, declaration.getParameterIndex()); @@ -469,7 +469,7 @@ private static class Converter implements Resolver { private final ArgumentConverter argumentConverter; - private static Converter defaultConverter(ExtensionContext context) { + private static Converter createDefault(ExtensionContext context) { return new Converter(new DefaultArgumentConverter(context)); } 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 7b858cacf13b..eea0e734508a 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 @@ -125,8 +125,8 @@ public final Object convert(Object source, Class targetType, ClassLoader clas } private LocaleConversionFormat getLocaleConversionFormat() { - return context.getConfigurationParameter(DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME, TRANSFORMER).orElse( - LocaleConversionFormat.BCP_47); + return context.getConfigurationParameter(DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME, TRANSFORMER) // + .orElse(LocaleConversionFormat.BCP_47); } Object convert(String source, Class targetType, ClassLoader classLoader) {