Skip to content

Remove built-in support for Locale conversion in ISO 639 format #4751

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ repository on GitHub.
* Deprecate `Launcher.execute(TestPlan, TestExecutionListener[])` and
`Launcher.execute(LauncherDiscoveryRequest, TestExecutionListener[])` in favor of
`Launcher.execute(LauncherExecutionRequest)`
* `ConversionSupport` now converts `String` to `Locale` using the IETF BCP 47 language tag
format supported by the `Locale.forLanguageTag(String)` factory method instead of the
format used by the deprecated `Locale(String)` constructor.

[[release-notes-6.0.0-M2-junit-platform-new-features-and-improvements]]
==== New Features and Improvements
Expand Down Expand Up @@ -65,6 +68,10 @@ repository on GitHub.
of the `TestTemplateInvocationContextProvider` interface from
`Stream<TestTemplateInvocationContext>` to
`Stream<? extends TestTemplateInvocationContext>`.
* Remove support for `junit.jupiter.params.arguments.conversion.locale.format`
configuration parameter. `Locale` conversions are now always performed using the IETF
BCP 47 language tag format supported by the `Locale.forLanguageTag(String)` factory
method.

[[release-notes-6.0.0-M2-junit-jupiter-new-features-and-improvements]]
==== New Features and Improvements
Expand Down
6 changes: 0 additions & 6 deletions documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2500,12 +2500,6 @@ 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 of version 5.12 and
earlier (which called the deprecated `Locale(String)` constructor), you can set the
`junit.jupiter.params.arguments.conversion.locale.format`
<<running-tests-config-params, configuration parameter>> 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@
@API(status = INTERNAL, since = "5.4")
public class DefaultJupiterConfiguration implements JupiterConfiguration {

private static final List<String> UNSUPPORTED_CONFIGURATION_PARAMETERS = List.of("junit.jupiter.tempdir.scope");
private static final List<String> UNSUPPORTED_CONFIGURATION_PARAMETERS = List.of( //
"junit.jupiter.tempdir.scope", //
"junit.jupiter.params.arguments.conversion.locale.format" //
);

private static final EnumConfigurationParameterConverter<ExecutionMode> executionModeConverter = //
new EnumConfigurationParameterConverter<>(ExecutionMode.class, "parallel execution mode");
Expand Down Expand Up @@ -88,9 +91,9 @@ public DefaultJupiterConfiguration(ConfigurationParameters configurationParamete
private void validateConfigurationParameters(DiscoveryIssueReporter issueReporter) {
UNSUPPORTED_CONFIGURATION_PARAMETERS.forEach(key -> configurationParameters.get(key) //
.ifPresent(value -> {
var warning = DiscoveryIssue.create(Severity.WARNING,
"The '%s' configuration parameter is no longer supported: %s. Please remove it from your configuration.".formatted(
key, value));
var warning = DiscoveryIssue.create(Severity.WARNING, """
The '%s' configuration parameter is no longer supported. \
Please remove it from your configuration.""".formatted(key));
issueReporter.reportIssue(warning);
}));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ private void storeParameterInfo(ExtensionContext context) {
ClassLoader classLoader = getClassLoader(this.declarationContext.getTestClass());
@Nullable
Object[] arguments = this.arguments.getConsumedPayloads();
ArgumentsAccessor accessor = DefaultArgumentsAccessor.create(context, invocationIndex, classLoader, arguments);
ArgumentsAccessor accessor = DefaultArgumentsAccessor.create(invocationIndex, classLoader, arguments);
new DefaultParameterInfo(declarations, accessor).store(context);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,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)
.orElseGet(() -> Converter.createDefault(extensionContext));
.orElse(Converter.DEFAULT);
} // @formatter:on
catch (Exception ex) {
throw parameterResolutionException("Error creating ArgumentConverter", ex, declaration.getParameterIndex());
Expand Down Expand Up @@ -479,9 +479,7 @@ Object resolve(FieldContext fieldContext, ExtensionContext extensionContext, Eva

private record Converter(ArgumentConverter argumentConverter) implements Resolver {

private static Converter createDefault(ExtensionContext context) {
return new Converter(new DefaultArgumentConverter(context));
}
static final Converter DEFAULT = new Converter(DefaultArgumentConverter.INSTANCE);

@Override
public @Nullable Object resolve(ParameterContext parameterContext, int parameterIndex,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@

import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
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;
Expand All @@ -41,13 +40,12 @@ public class DefaultArgumentsAccessor implements ArgumentsAccessor {
private final @Nullable Object[] arguments;
private final BiFunction<@Nullable Object, Class<?>, @Nullable Object> converter;

public static DefaultArgumentsAccessor create(ExtensionContext context, int invocationIndex,
ClassLoader classLoader, @Nullable Object[] arguments) {
public static DefaultArgumentsAccessor create(int invocationIndex, ClassLoader classLoader,
@Nullable Object[] arguments) {
Preconditions.notNull(classLoader, "ClassLoader must not be null");

BiFunction<@Nullable Object, Class<?>, @Nullable Object> converter = (source,
targetType) -> new DefaultArgumentConverter(context) //
.convert(source, targetType, classLoader);
targetType) -> DefaultArgumentConverter.INSTANCE.convert(source, targetType, classLoader);
return new DefaultArgumentsAccessor(converter, invocationIndex, arguments);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,9 @@
import java.util.Currency;
import java.util.Locale;
import java.util.UUID;
import java.util.function.Function;

import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
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;
Expand Down Expand Up @@ -53,31 +51,9 @@
@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}
*
* <h4>Supported Values</h4>
* <ul>
* <li>{@code bcp_47}: uses the IETF BCP 47 language tag format, delegating
* the conversion to {@link Locale#forLanguageTag(String)}</li>
* <li>{@code iso_639}: uses the ISO 639 alpha-2 or alpha-3 language code
* format, delegating the conversion to {@link Locale#Locale(String)}</li>
* </ul>
*
* <p>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<String, LocaleConversionFormat> TRANSFORMER = value -> LocaleConversionFormat.valueOf(
value.strip().toUpperCase(Locale.ROOT));

private final ExtensionContext context;

public DefaultArgumentConverter(ExtensionContext context) {
this.context = context;
public static final DefaultArgumentConverter INSTANCE = new DefaultArgumentConverter();

private DefaultArgumentConverter() {
}

@Override
Expand Down Expand Up @@ -110,10 +86,6 @@ public DefaultArgumentConverter(ExtensionContext context) {
}

if (source instanceof String string) {
if (targetType == Locale.class && getLocaleConversionFormat() == LocaleConversionFormat.BCP_47) {
return Locale.forLanguageTag(string);
}

try {
return convert(string, targetType, classLoader);
}
Expand All @@ -126,22 +98,9 @@ public DefaultArgumentConverter(ExtensionContext context) {
source.getClass().getTypeName(), targetType.getTypeName()));
}

private LocaleConversionFormat getLocaleConversionFormat() {
return context.getConfigurationParameter(DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME, TRANSFORMER) //
.orElse(LocaleConversionFormat.BCP_47);
}

@Nullable
Object convert(@Nullable String source, Class<?> targetType, ClassLoader classLoader) {
return ConversionSupport.convert(source, targetType, classLoader);
}

enum LocaleConversionFormat {

BCP_47,

ISO_639

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@

class StringToCommonJavaTypesConverter implements StringToObjectConverter {

@SuppressWarnings("deprecation")
private static final Map<Class<?>, Function<String, ?>> CONVERTERS = Map.of( //
// java.io and java.nio
File.class, File::new, //
Expand All @@ -38,7 +37,7 @@ class StringToCommonJavaTypesConverter implements StringToObjectConverter {
URL.class, StringToCommonJavaTypesConverter::toURL,
// java.util
Currency.class, Currency::getInstance, //
Locale.class, Locale::new, //
Locale.class, Locale::forLanguageTag, //
UUID.class, UUID::fromString //
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,9 @@ void usingTheRemovedScopeConfigurationParameterProducesWarning() {
.configurationParameter("junit.jupiter.tempdir.scope", "per_context"));

assertThat(results.getDiscoveryIssues()) //
.contains(DiscoveryIssue.create(Severity.WARNING,
"The 'junit.jupiter.tempdir.scope' configuration parameter is no longer supported: per_context. Please remove it from your configuration."));
.contains(DiscoveryIssue.create(Severity.WARNING, """
The 'junit.jupiter.tempdir.scope' configuration parameter is no longer supported. \
Please remove it from your configuration."""));
}

@Nested
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
package org.junit.jupiter.params;

import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static java.util.Objects.requireNonNull;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.within;
import static org.junit.jupiter.api.Assertions.assertEquals;
Expand All @@ -22,7 +23,6 @@
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.selectIteration;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod;
Expand Down Expand Up @@ -99,6 +99,7 @@
import org.junit.jupiter.params.converter.ArgumentConversionException;
import org.junit.jupiter.params.converter.ArgumentConverter;
import org.junit.jupiter.params.converter.ConvertWith;
import org.junit.jupiter.params.converter.TypedArgumentConverter;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.provider.ArgumentsSource;
Expand All @@ -114,6 +115,8 @@
import org.junit.jupiter.params.support.ParameterDeclarations;
import org.junit.platform.commons.PreconditionViolationException;
import org.junit.platform.commons.util.ClassUtils;
import org.junit.platform.engine.DiscoveryIssue;
import org.junit.platform.engine.DiscoveryIssue.Severity;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.testkit.engine.EngineExecutionResults;
Expand Down Expand Up @@ -488,17 +491,20 @@ void executesWithDefaultLocaleConversionFormat() {
}

@Test
void executesWithBcp47LocaleConversionFormat() {
var results = execute(Map.of(DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME, "bcp_47"),
LocaleConversionTestCase.class, "testWithBcp47", Locale.class);
void emitsWarningForNoLongerSupportedConfigurationParameter() {
var results = discoverTests(request -> request //
.configurationParameter("junit.jupiter.params.arguments.conversion.locale.format", "iso_639") //
.selectors(selectMethod(LocaleConversionTestCase.class, "testWithBcp47", Locale.class)));

results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4));
assertThat(results.getDiscoveryIssues()) //
.contains(DiscoveryIssue.create(Severity.WARNING, """
The 'junit.jupiter.params.arguments.conversion.locale.format' configuration parameter \
is no longer supported. Please remove it from your configuration."""));
}

@Test
void executesWithIso639LocaleConversionFormat() {
var results = execute(Map.of(DEFAULT_LOCALE_CONVERSION_FORMAT_PROPERTY_NAME, "iso_639"),
LocaleConversionTestCase.class, "testWithIso639", Locale.class);
void executesWithCustomLocalConverterUsingIso639Format() {
var results = execute(LocaleConversionTestCase.class, "testWithIso639", Locale.class);

results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4));
}
Expand Down Expand Up @@ -2566,11 +2572,24 @@ void testWithBcp47(Locale locale) {

@ParameterizedTest
@ValueSource(strings = "en-US")
void testWithIso639(Locale locale) {
void testWithIso639(@ConvertWith(Iso639Converter.class) Locale locale) {
assertEquals("en-us", locale.getLanguage());
assertEquals("", locale.getCountry());
}

static class Iso639Converter extends TypedArgumentConverter<String, Locale> {

Iso639Converter() {
super(String.class, Locale.class);
}

@SuppressWarnings("deprecation")
@Override
protected Locale convert(@Nullable String source) throws ArgumentConversionException {
return new Locale(requireNonNull(source));
}
}

}

private static class TwoSingleStringArgumentsProvider implements ArgumentsProvider {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ private static DefaultArgumentsAccessor defaultArgumentsAccessor(int invocationI
@Nullable Object... arguments) {
var context = mock(ExtensionContext.class);
var classLoader = DefaultArgumentsAccessorTests.class.getClassLoader();
return DefaultArgumentsAccessor.create(context, invocationIndex, classLoader, arguments);
return DefaultArgumentsAccessor.create(invocationIndex, classLoader, arguments);
}

@SuppressWarnings("unused")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,16 @@

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.jspecify.annotations.Nullable;
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;
Expand All @@ -46,8 +36,7 @@
*/
class DefaultArgumentConverterTests {

private final ExtensionContext context = mock();
private final DefaultArgumentConverter underTest = spy(new DefaultArgumentConverter(context));
private final DefaultArgumentConverter underTest = spy(DefaultArgumentConverter.INSTANCE);

@Test
void isAwareOfNull() {
Expand Down Expand Up @@ -112,36 +101,6 @@ 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");
Expand Down
Loading