Skip to content

Support CharSequence argument for Fallback String-to-Object Conversion #4819

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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,7 +26,9 @@ repository on GitHub.
[[release-notes-6.0.0-RC1-junit-platform-new-features-and-improvements]]
==== New Features and Improvements

* ❓
* Convention-based conversion in `ConversionSupport` now supports factory methods and
factory constructors that accept a single `CharSequence` argument in addition to the
existing support for factories that accept a single `String` argument.


[[release-notes-6.0.0-RC1-junit-jupiter]]
Expand All @@ -53,6 +55,10 @@ repository on GitHub.
In addition, special characters are escaped within quoted text. Please refer to the
<<../user-guide/index.adoc#writing-tests-parameterized-tests-display-names-quoted-text,
User Guide>> for details.
* <<../user-guide/index.adoc#writing-tests-parameterized-tests-argument-conversion-implicit-fallback,
Fallback String-to-Object Conversion>> for parameterized tests now supports factory
methods and factory constructors that accept a single `CharSequence` argument in
addition to the existing support for factories that accept a single `String` argument.


[[release-notes-6.0.0-RC1-junit-vintage]]
Expand Down
9 changes: 5 additions & 4 deletions documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2535,11 +2535,12 @@ table, JUnit Jupiter also provides a fallback mechanism for automatic conversion
method_ or a _factory constructor_ as defined below.

- __factory method__: a non-private, `static` method declared in the target type that
accepts a single `String` argument and returns an instance of the target type. The name
of the method can be arbitrary and need not follow any particular convention.
accepts either a single `String` argument or a single `CharSequence` argument and
returns an instance of the target type. The name of the method can be arbitrary and need
not follow any particular convention.
- __factory constructor__: a non-private constructor in the target type that accepts a
single `String` argument. Note that the target type must be declared as either a
top-level class or as a `static` nested class.
either a single `String` argument or a single `CharSequence` argument. Note that the
target type must be declared as either a top-level class or as a `static` nested class.

NOTE: If multiple _factory methods_ are discovered, they will be ignored. If a _factory
method_ and a _factory constructor_ are discovered, the factory method will be used
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,20 @@ private ConversionSupport() {
*
* <ol>
* <li>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.</li>
* type that converts from a {@link String} to the target type. Use the
* factory method if present.</li>
* <li>Search for a single, non-private constructor in the target type that
* accepts a String. Use the constructor if present.</li>
* accepts a {@link String}. Use the constructor if present.</li>
* <li>Search for a single, non-private static factory method in the target
* type that converts from a {@link CharSequence} to the target type. Use the
* factory method if present.</li>
* <li>Search for a single, non-private constructor in the target type that
* accepts a {@link CharSequence}. Use the constructor if present.</li>
* </ol>
*
* <p>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.
* <p>If multiple suitable factory methods or constructors 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,21 @@
* <h2>Search Algorithm</h2>
*
* <ol>
* <li>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
* <li>Search for a single, non-private static factory method in the target type
* that converts from a {@link String} to the target type. Use the factory method
* if present.</li>
* <li>Search for a single, non-private constructor in the target type that
* accepts a String. Use the constructor if present.</li>
* <li>Search for a single, non-private constructor in the target type that accepts
* a {@link String}. Use the constructor if present.</li>
* <li>Search for a single, non-private static factory method in the target type
* that converts from a {@link CharSequence} to the target type. Use the factory
* method if present.</li>
* <li>Search for a single, non-private constructor in the target type that accepts
* a {@link CharSequence}. Use the constructor if present.</li>
* </ol>
*
* <p>If multiple suitable factory methods are discovered they will be ignored.
* If neither a single factory method nor a single constructor is found, this
* converter acts as a no-op.
* <p>If multiple suitable factory methods or constructors are discovered they
* will be ignored. If neither a single factory method nor a single constructor
* is found, this converter acts as a no-op.
*
* @since 1.11
* @see ConversionSupport
Expand Down Expand Up @@ -86,28 +91,47 @@ public boolean canConvertTo(Class<?> targetType) {

private static Function<String, @Nullable Object> findFactoryExecutable(Class<?> targetType) {
return factoryExecutableCache.computeIfAbsent(targetType, type -> {
Method factoryMethod = findFactoryMethod(type);
if (factoryMethod != null) {
return source -> invokeMethod(factoryMethod, null, source);
// First, search for exact String argument matches.
Function<String, @Nullable Object> factory = findFactoryExecutable(type, String.class);
if (factory != null) {
return factory;
}
Constructor<?> constructor = findFactoryConstructor(type);
if (constructor != null) {
return source -> newInstance(constructor, source);
// Second, fall back to CharSequence argument matches.
factory = findFactoryExecutable(type, CharSequence.class);
if (factory != null) {
return factory;
}
// Else, nothing found.
return NULL_EXECUTABLE;
});
}

private static @Nullable Method findFactoryMethod(Class<?> targetType) {
List<Method> factoryMethods = findMethods(targetType, new IsFactoryMethod(targetType), BOTTOM_UP);
private static @Nullable Function<String, @Nullable Object> findFactoryExecutable(Class<?> targetType,
Class<?> parameterType) {

Method factoryMethod = findFactoryMethod(targetType, parameterType);
if (factoryMethod != null) {
return source -> invokeMethod(factoryMethod, null, source);
}
Constructor<?> constructor = findFactoryConstructor(targetType, parameterType);
if (constructor != null) {
return source -> newInstance(constructor, source);
}
return null;
}

private static @Nullable Method findFactoryMethod(Class<?> targetType, Class<?> parameterType) {
List<Method> factoryMethods = findMethods(targetType, new IsFactoryMethod(targetType, parameterType),
BOTTOM_UP);
if (factoryMethods.size() == 1) {
return factoryMethods.get(0);
}
return null;
}

private static @Nullable Constructor<?> findFactoryConstructor(Class<?> targetType) {
List<Constructor<?>> constructors = findConstructors(targetType, new IsFactoryConstructor(targetType));
private static @Nullable Constructor<?> findFactoryConstructor(Class<?> targetType, Class<?> parameterType) {
List<Constructor<?>> constructors = findConstructors(targetType,
new IsFactoryConstructor(targetType, parameterType));
if (constructors.size() == 1) {
return constructors.get(0);
}
Expand All @@ -117,15 +141,9 @@ public boolean canConvertTo(Class<?> targetType) {
/**
* {@link Predicate} that determines if the {@link Method} supplied to
* {@link #test(Method)} is a non-private static factory method for the
* supplied {@link #targetType}.
* supplied {@link #targetType} and {@link #parameterType}.
*/
static class IsFactoryMethod implements Predicate<Method> {

private final Class<?> targetType;

IsFactoryMethod(Class<?> targetType) {
this.targetType = targetType;
}
record IsFactoryMethod(Class<?> targetType, Class<?> parameterType) implements Predicate<Method> {

@Override
public boolean test(Method method) {
Expand All @@ -136,39 +154,35 @@ public boolean test(Method method) {
if (isNotStatic(method)) {
return false;
}
return isNotPrivateAndAcceptsSingleStringArgument(method);
return isFactoryCandidate(method, this.parameterType);
}

}

/**
* {@link Predicate} that determines if the {@link Constructor} supplied to
* {@link #test(Constructor)} is a non-private factory constructor for the
* supplied {@link #targetType}.
* supplied {@link #targetType} and {@link #parameterType}.
*/
static class IsFactoryConstructor implements Predicate<Constructor<?>> {

private final Class<?> targetType;

IsFactoryConstructor(Class<?> targetType) {
this.targetType = targetType;
}
record IsFactoryConstructor(Class<?> targetType, Class<?> parameterType) implements Predicate<Constructor<?>> {

@Override
public boolean test(Constructor<?> constructor) {
// Please do not collapse the following into a single statement.
if (!constructor.getDeclaringClass().equals(this.targetType)) {
return false;
}
return isNotPrivateAndAcceptsSingleStringArgument(constructor);
return isFactoryCandidate(constructor, this.parameterType);
}

}

private static boolean isNotPrivateAndAcceptsSingleStringArgument(Executable executable) {
/**
* Determine if the supplied {@link Executable} is not private and accepts a
* single argument of the supplied parameter type.
*/
private static boolean isFactoryCandidate(Executable executable, Class<?> parameterType) {
return isNotPrivate(executable) //
&& (executable.getParameterCount() == 1) //
&& (executable.getParameterTypes()[0] == String.class);
&& (executable.getParameterTypes()[0] == parameterType);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,17 @@ void executesWithImplicitGenericConverter() {
event(test(), displayName("[2] book = book 2"), finishedWithFailure(message("book 2"))));
}

/**
* @since 6.0
*/
@Test
void executesWithImplicitGenericConverterWithCharSequenceConstructor() {
var results = execute("testWithImplicitGenericConverterWithCharSequenceConstructor", Record.class);
results.testEvents().assertThatEvents() //
.haveExactly(1, event(displayName("\"record 1\""), finishedWithFailure(message("record 1")))) //
.haveExactly(1, event(displayName("\"record 2\""), finishedWithFailure(message("record 2"))));
}

@Test
void legacyReportingNames() {
var results = execute("testWithCustomName", String.class, int.class);
Expand Down Expand Up @@ -1460,6 +1471,12 @@ void testWithImplicitGenericConverter(Book book) {
fail(book.title);
}

@ParameterizedTest(name = "{0}")
@ValueSource(strings = { "record 1", "record 2" })
void testWithImplicitGenericConverterWithCharSequenceConstructor(Record record) {
fail(record.title.toString());
}

@ParameterizedTest(quoteTextArguments = false)
@ValueSource(strings = { "O", "XXX" })
void testWithExplicitConverter(@ConvertWith(StringLengthConverter.class) int length) {
Expand Down Expand Up @@ -2673,6 +2690,9 @@ static Book factory(String title) {
}
}

record Record(CharSequence title) {
}

static class FailureInBeforeEachTestCase {

@BeforeEach
Expand Down
Loading
Loading