Skip to content

Commit 8e7df9d

Browse files
committed
Support CharSequence argument for Fallback String-to-Object Conversion
Prior to this commit, the Fallback String-to-Object Conversion support for parameterized tests supported factory constructors and methods that accepted a single String argument. This commit relaxes that restriction to support factory constructors and methods that accept either a single String argument or a single CharSequence argument, since the original source String can always be supplied to an Executable that accepts a CharSequence. For backward compatibility, the search algorithm gives precedence to factories that accept String arguments, only falling back to factories that accept CharSequence arguments if necessary. Note that this change is available to third parties via ConversionSupport in junit-platform-commons, which junit-jupiter-params utilizes. Closes #4815
1 parent ad9a303 commit 8e7df9d

File tree

6 files changed

+228
-62
lines changed

6 files changed

+228
-62
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-6.0.0-RC1.adoc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ repository on GitHub.
2727
==== New Features and Improvements
2828

2929
* Prune stack traces up to test or lifecycle method
30+
* Convention-based conversion in `ConversionSupport` now supports factory methods and
31+
factory constructors that accept a single `CharSequence` argument in addition to the
32+
existing support for factories that accept a single `String` argument.
33+
3034

3135
[[release-notes-6.0.0-RC1-junit-jupiter]]
3236
=== JUnit Jupiter
@@ -52,6 +56,10 @@ repository on GitHub.
5256
In addition, special characters are escaped within quoted text. Please refer to the
5357
<<../user-guide/index.adoc#writing-tests-parameterized-tests-display-names-quoted-text,
5458
User Guide>> for details.
59+
* <<../user-guide/index.adoc#writing-tests-parameterized-tests-argument-conversion-implicit-fallback,
60+
Fallback String-to-Object Conversion>> for parameterized tests now supports factory
61+
methods and factory constructors that accept a single `CharSequence` argument in
62+
addition to the existing support for factories that accept a single `String` argument.
5563

5664

5765
[[release-notes-6.0.0-RC1-junit-vintage]]

documentation/src/docs/asciidoc/user-guide/writing-tests.adoc

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2519,11 +2519,12 @@ table, JUnit Jupiter also provides a fallback mechanism for automatic conversion
25192519
method_ or a _factory constructor_ as defined below.
25202520

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

25282529
NOTE: If multiple _factory methods_ are discovered, they will be ignored. If a _factory
25292530
method_ and a _factory constructor_ are discovered, the factory method will be used

junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/ConversionSupport.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,20 @@ private ConversionSupport() {
7575
*
7676
* <ol>
7777
* <li>Search for a single, non-private static factory method in the target
78-
* type that converts from a String to the target type. Use the factory method
79-
* if present.</li>
78+
* type that converts from a {@link String} to the target type. Use the
79+
* factory method if present.</li>
8080
* <li>Search for a single, non-private constructor in the target type that
81-
* accepts a String. Use the constructor if present.</li>
81+
* accepts a {@link String}. Use the constructor if present.</li>
82+
* <li>Search for a single, non-private static factory method in the target
83+
* type that converts from a {@link CharSequence} to the target type. Use the
84+
* factory method if present.</li>
85+
* <li>Search for a single, non-private constructor in the target type that
86+
* accepts a {@link CharSequence}. Use the constructor if present.</li>
8287
* </ol>
8388
*
84-
* <p>If multiple suitable factory methods are discovered they will be ignored.
85-
* If neither a single factory method nor a single constructor is found, the
86-
* convention-based conversion strategy will not apply.
89+
* <p>If multiple suitable factory methods or constructors are discovered they
90+
* will be ignored. If neither a single factory method nor a single constructor
91+
* is found, the convention-based conversion strategy will not apply.
8792
*
8893
* @param source the source {@code String} to convert; may be {@code null}
8994
* but only if the target type is a reference type

junit-platform-commons/src/main/java/org/junit/platform/commons/support/conversion/FallbackStringToObjectConverter.java

Lines changed: 53 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,21 @@
3838
* <h2>Search Algorithm</h2>
3939
*
4040
* <ol>
41-
* <li>Search for a single, non-private static factory method in the target
42-
* type that converts from a String to the target type. Use the factory method
41+
* <li>Search for a single, non-private static factory method in the target type
42+
* that converts from a {@link String} to the target type. Use the factory method
4343
* if present.</li>
44-
* <li>Search for a single, non-private constructor in the target type that
45-
* accepts a String. Use the constructor if present.</li>
44+
* <li>Search for a single, non-private constructor in the target type that accepts
45+
* a {@link String}. Use the constructor if present.</li>
46+
* <li>Search for a single, non-private static factory method in the target type
47+
* that converts from a {@link CharSequence} to the target type. Use the factory
48+
* method if present.</li>
49+
* <li>Search for a single, non-private constructor in the target type that accepts
50+
* a {@link CharSequence}. Use the constructor if present.</li>
4651
* </ol>
4752
*
48-
* <p>If multiple suitable factory methods are discovered they will be ignored.
49-
* If neither a single factory method nor a single constructor is found, this
50-
* converter acts as a no-op.
53+
* <p>If multiple suitable factory methods or constructors are discovered they
54+
* will be ignored. If neither a single factory method nor a single constructor
55+
* is found, this converter acts as a no-op.
5156
*
5257
* @since 1.11
5358
* @see ConversionSupport
@@ -86,28 +91,47 @@ public boolean canConvertTo(Class<?> targetType) {
8691

8792
private static Function<String, @Nullable Object> findFactoryExecutable(Class<?> targetType) {
8893
return factoryExecutableCache.computeIfAbsent(targetType, type -> {
89-
Method factoryMethod = findFactoryMethod(type);
90-
if (factoryMethod != null) {
91-
return source -> invokeMethod(factoryMethod, null, source);
94+
// First, search for exact String argument matches.
95+
Function<String, @Nullable Object> factory = findFactoryExecutable(type, String.class);
96+
if (factory != null) {
97+
return factory;
9298
}
93-
Constructor<?> constructor = findFactoryConstructor(type);
94-
if (constructor != null) {
95-
return source -> newInstance(constructor, source);
99+
// Second, fall back to CharSequence argument matches.
100+
factory = findFactoryExecutable(type, CharSequence.class);
101+
if (factory != null) {
102+
return factory;
96103
}
104+
// Else, nothing found.
97105
return NULL_EXECUTABLE;
98106
});
99107
}
100108

101-
private static @Nullable Method findFactoryMethod(Class<?> targetType) {
102-
List<Method> factoryMethods = findMethods(targetType, new IsFactoryMethod(targetType), BOTTOM_UP);
109+
private static @Nullable Function<String, @Nullable Object> findFactoryExecutable(Class<?> targetType,
110+
Class<?> parameterType) {
111+
112+
Method factoryMethod = findFactoryMethod(targetType, parameterType);
113+
if (factoryMethod != null) {
114+
return source -> invokeMethod(factoryMethod, null, source);
115+
}
116+
Constructor<?> constructor = findFactoryConstructor(targetType, parameterType);
117+
if (constructor != null) {
118+
return source -> newInstance(constructor, source);
119+
}
120+
return null;
121+
}
122+
123+
private static @Nullable Method findFactoryMethod(Class<?> targetType, Class<?> parameterType) {
124+
List<Method> factoryMethods = findMethods(targetType, new IsFactoryMethod(targetType, parameterType),
125+
BOTTOM_UP);
103126
if (factoryMethods.size() == 1) {
104127
return factoryMethods.get(0);
105128
}
106129
return null;
107130
}
108131

109-
private static @Nullable Constructor<?> findFactoryConstructor(Class<?> targetType) {
110-
List<Constructor<?>> constructors = findConstructors(targetType, new IsFactoryConstructor(targetType));
132+
private static @Nullable Constructor<?> findFactoryConstructor(Class<?> targetType, Class<?> parameterType) {
133+
List<Constructor<?>> constructors = findConstructors(targetType,
134+
new IsFactoryConstructor(targetType, parameterType));
111135
if (constructors.size() == 1) {
112136
return constructors.get(0);
113137
}
@@ -117,15 +141,9 @@ public boolean canConvertTo(Class<?> targetType) {
117141
/**
118142
* {@link Predicate} that determines if the {@link Method} supplied to
119143
* {@link #test(Method)} is a non-private static factory method for the
120-
* supplied {@link #targetType}.
144+
* supplied {@link #targetType} and {@link #parameterType}.
121145
*/
122-
static class IsFactoryMethod implements Predicate<Method> {
123-
124-
private final Class<?> targetType;
125-
126-
IsFactoryMethod(Class<?> targetType) {
127-
this.targetType = targetType;
128-
}
146+
record IsFactoryMethod(Class<?> targetType, Class<?> parameterType) implements Predicate<Method> {
129147

130148
@Override
131149
public boolean test(Method method) {
@@ -136,39 +154,35 @@ public boolean test(Method method) {
136154
if (isNotStatic(method)) {
137155
return false;
138156
}
139-
return isNotPrivateAndAcceptsSingleStringArgument(method);
157+
return isFactoryCandidate(method, this.parameterType);
140158
}
141-
142159
}
143160

144161
/**
145162
* {@link Predicate} that determines if the {@link Constructor} supplied to
146163
* {@link #test(Constructor)} is a non-private factory constructor for the
147-
* supplied {@link #targetType}.
164+
* supplied {@link #targetType} and {@link #parameterType}.
148165
*/
149-
static class IsFactoryConstructor implements Predicate<Constructor<?>> {
150-
151-
private final Class<?> targetType;
152-
153-
IsFactoryConstructor(Class<?> targetType) {
154-
this.targetType = targetType;
155-
}
166+
record IsFactoryConstructor(Class<?> targetType, Class<?> parameterType) implements Predicate<Constructor<?>> {
156167

157168
@Override
158169
public boolean test(Constructor<?> constructor) {
159170
// Please do not collapse the following into a single statement.
160171
if (!constructor.getDeclaringClass().equals(this.targetType)) {
161172
return false;
162173
}
163-
return isNotPrivateAndAcceptsSingleStringArgument(constructor);
174+
return isFactoryCandidate(constructor, this.parameterType);
164175
}
165-
166176
}
167177

168-
private static boolean isNotPrivateAndAcceptsSingleStringArgument(Executable executable) {
178+
/**
179+
* Determine if the supplied {@link Executable} is not private and accepts a
180+
* single argument of the supplied parameter type.
181+
*/
182+
private static boolean isFactoryCandidate(Executable executable, Class<?> parameterType) {
169183
return isNotPrivate(executable) //
170184
&& (executable.getParameterCount() == 1) //
171-
&& (executable.getParameterTypes()[0] == String.class);
185+
&& (executable.getParameterTypes()[0] == parameterType);
172186
}
173187

174188
}

jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,17 @@ void executesWithImplicitGenericConverter() {
311311
event(test(), displayName("[2] book = book 2"), finishedWithFailure(message("book 2"))));
312312
}
313313

314+
/**
315+
* @since 6.0
316+
*/
317+
@Test
318+
void executesWithImplicitGenericConverterWithCharSequenceConstructor() {
319+
var results = execute("testWithImplicitGenericConverterWithCharSequenceConstructor", Record.class);
320+
results.testEvents().assertThatEvents() //
321+
.haveExactly(1, event(displayName("\"record 1\""), finishedWithFailure(message("record 1")))) //
322+
.haveExactly(1, event(displayName("\"record 2\""), finishedWithFailure(message("record 2"))));
323+
}
324+
314325
@Test
315326
void legacyReportingNames() {
316327
var results = execute("testWithCustomName", String.class, int.class);
@@ -1460,6 +1471,12 @@ void testWithImplicitGenericConverter(Book book) {
14601471
fail(book.title);
14611472
}
14621473

1474+
@ParameterizedTest(name = "{0}")
1475+
@ValueSource(strings = { "record 1", "record 2" })
1476+
void testWithImplicitGenericConverterWithCharSequenceConstructor(Record record) {
1477+
fail(record.title.toString());
1478+
}
1479+
14631480
@ParameterizedTest(quoteTextArguments = false)
14641481
@ValueSource(strings = { "O", "XXX" })
14651482
void testWithExplicitConverter(@ConvertWith(StringLengthConverter.class) int length) {
@@ -2673,6 +2690,9 @@ static Book factory(String title) {
26732690
}
26742691
}
26752692

2693+
record Record(CharSequence title) {
2694+
}
2695+
26762696
static class FailureInBeforeEachTestCase {
26772697

26782698
@BeforeEach

0 commit comments

Comments
 (0)