Skip to content

Commit 1c40f91

Browse files
committed
fix: validate argument count for @ParameterizedClass field injection (#5079)
1 parent 615254f commit 1c40f91

File tree

2 files changed

+96
-0
lines changed

2 files changed

+96
-0
lines changed

junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassContext.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
import java.util.List;
2828
import java.util.function.Predicate;
2929

30+
import org.jspecify.annotations.Nullable;
3031
import org.junit.jupiter.api.TestInstance;
32+
import org.junit.jupiter.api.extension.ParameterResolutionException;
3133
import org.junit.jupiter.params.provider.Arguments;
3234
import org.junit.platform.commons.JUnitException;
3335
import org.junit.platform.commons.support.HierarchyTraversalMode;
@@ -130,6 +132,11 @@ public ResolverFacade getResolverFacade() {
130132
@Override
131133
public ParameterizedClassInvocationContext createInvocationContext(ParameterizedInvocationNameFormatter formatter,
132134
Arguments arguments, int invocationIndex) {
135+
136+
if (this.injectionType == InjectionType.FIELDS) {
137+
assertEnoughArgumentsForFieldInjection(arguments);
138+
}
139+
133140
return new ParameterizedClassInvocationContext(this, formatter, arguments, invocationIndex);
134141
}
135142

@@ -172,6 +179,65 @@ private static <A extends Annotation> A getAnnotation(Method method, Class<A> an
172179
.orElseThrow(() -> new JUnitException("Method not annotated with @" + annotationType.getSimpleName()));
173180
}
174181

182+
private void assertEnoughArgumentsForFieldInjection(Arguments arguments) {
183+
@SuppressWarnings("NullAway")
184+
final Object[] providedArguments = (arguments == null ? new Object[0] : arguments.get());
185+
final int providedArgumentCount = providedArguments.length;
186+
187+
final List<Field> parameterFields = findParameterAnnotatedFields(this.testClass);
188+
if (parameterFields.isEmpty()) {
189+
return;
190+
}
191+
192+
final int requiredArgumentCount = requiredArgumentCountForParameterFields(parameterFields);
193+
if (providedArgumentCount >= requiredArgumentCount) {
194+
return;
195+
}
196+
197+
final @Nullable Field firstMissingField = firstMissingParameterFieldByIndex(parameterFields,
198+
providedArgumentCount);
199+
200+
final String missingTargetDescription = (firstMissingField != null)
201+
? "field '%s' (index %d, type %s)".formatted(firstMissingField.getName(),
202+
firstMissingField.getAnnotation(Parameter.class).value(), firstMissingField.getType().getName())
203+
: "parameter at index %d".formatted(providedArgumentCount);
204+
205+
throw new ParameterResolutionException(
206+
"Not enough arguments for @ParameterizedClass field injection in %s: %s cannot be injected because only %d argument(s) were provided; at least %d are required.".formatted(
207+
this.testClass.getName(), missingTargetDescription, providedArgumentCount, requiredArgumentCount));
208+
}
209+
210+
private static int requiredArgumentCountForParameterFields(List<Field> parameterFields) {
211+
int maxIndex = -1;
212+
for (Field field : parameterFields) {
213+
Parameter param = field.getAnnotation(Parameter.class);
214+
if (param != null) {
215+
int index = param.value();
216+
if (index >= 0 && index > maxIndex) {
217+
maxIndex = index;
218+
}
219+
}
220+
}
221+
return maxIndex + 1;
222+
}
223+
224+
private static @Nullable Field firstMissingParameterFieldByIndex(List<Field> parameterFields,
225+
int providedArgumentCount) {
226+
Field candidate = null;
227+
int minIndex = Integer.MAX_VALUE;
228+
for (Field field : parameterFields) {
229+
Parameter param = field.getAnnotation(Parameter.class);
230+
if (param != null) {
231+
int index = param.value();
232+
if (index >= 0 && index >= providedArgumentCount && index < minIndex) {
233+
candidate = field;
234+
minIndex = index;
235+
}
236+
}
237+
}
238+
return candidate;
239+
}
240+
175241
enum InjectionType {
176242
CONSTRUCTOR, FIELDS
177243
}

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,20 @@ void declaredIndexMustBeUnique() {
552552
"Configuration error: duplicate index declared in @Parameter(0) annotation on fields [int %s.i, long %s.l].".formatted(
553553
classTemplateClass.getName(), classTemplateClass.getName()))));
554554
}
555+
556+
@Test
557+
void failsWithMeaningfulErrorWhenTooFewArgumentsProvidedForFieldInjection() {
558+
var results = executeTestsForClass(NotEnoughArgumentsForFieldsTestCase.class);
559+
560+
results.containerEvents().assertThatEvents()
561+
.haveExactly(1, event(finishedWithFailure(
562+
instanceOf(org.junit.jupiter.api.extension.ParameterResolutionException.class),
563+
message(it -> it.contains("field 's")
564+
&& it.contains("index 1")
565+
&& it.contains("only 1 argument")
566+
&& it.contains("at least 2"))
567+
)));
568+
}
555569
}
556570

557571
@Nested
@@ -1693,6 +1707,22 @@ void test() {
16931707
}
16941708
}
16951709

1710+
@ParameterizedClass
1711+
@ValueSource(ints = 1)
1712+
static class NotEnoughArgumentsForFieldsTestCase {
1713+
1714+
@Parameter(0)
1715+
int i;
1716+
1717+
@Parameter(1)
1718+
String s;
1719+
1720+
@org.junit.jupiter.api.Test
1721+
void test() {
1722+
fail("should not be called");
1723+
}
1724+
}
1725+
16961726
@ParameterizedClass
16971727
@CsvSource({ "unused1, foo, unused2, bar", "unused4, baz, unused5, qux" })
16981728
static class InvalidUnusedParameterIndexesTestCase {

0 commit comments

Comments
 (0)