Skip to content

Commit cdbdf42

Browse files
Validate that there are enough arguments to inject @Parameter fields
`ArgumentCountValidator` now also validates that there are enough arguments for all required parameters which is currently only the case for indexed `@Parameter` fields. Fixes #5079. --------- Co-authored-by: Marc Philipp <mail@marcphilipp.de>
1 parent c8c405f commit cdbdf42

File tree

4 files changed

+79
-14
lines changed

4 files changed

+79
-14
lines changed

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

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import java.util.Arrays;
1414
import java.util.Optional;
1515

16+
import org.jspecify.annotations.Nullable;
1617
import org.junit.jupiter.api.extension.ExtensionConfigurationException;
1718
import org.junit.jupiter.api.extension.ExtensionContext;
1819
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
@@ -36,6 +37,7 @@ class ArgumentCountValidator {
3637
}
3738

3839
void validate(ExtensionContext extensionContext) {
40+
validateRequiredArgumentsArePresent();
3941
ArgumentCountValidationMode argumentCountValidationMode = getArgumentCountValidationMode(extensionContext);
4042
switch (argumentCountValidationMode) {
4143
case DEFAULT, NONE -> {
@@ -45,17 +47,34 @@ void validate(ExtensionContext extensionContext) {
4547
this.arguments);
4648
int totalCount = this.arguments.getTotalLength();
4749
Preconditions.condition(consumedCount == totalCount,
48-
() -> "Configuration error: @%s consumes %s %s but there %s %s %s provided.%nNote: the provided arguments were %s".formatted(
49-
this.declarationContext.getAnnotationName(), consumedCount,
50-
pluralize(consumedCount, "parameter", "parameters"), pluralize(totalCount, "was", "were"),
51-
totalCount, pluralize(totalCount, "argument", "arguments"),
52-
Arrays.toString(this.arguments.getAllPayloads())));
50+
() -> wrongNumberOfArgumentsMessages("consumes", consumedCount, null, null));
5351
}
5452
default -> throw new ExtensionConfigurationException(
5553
"Unsupported argument count validation mode: " + argumentCountValidationMode);
5654
}
5755
}
5856

57+
private void validateRequiredArgumentsArePresent() {
58+
var requiredParameterCount = this.declarationContext.getResolverFacade().getRequiredParameterCount();
59+
if (requiredParameterCount != null) {
60+
var totalCount = this.arguments.getTotalLength();
61+
Preconditions.condition(requiredParameterCount.value() <= totalCount,
62+
() -> wrongNumberOfArgumentsMessages("has", requiredParameterCount.value(), "required",
63+
requiredParameterCount.reason()));
64+
}
65+
}
66+
67+
private String wrongNumberOfArgumentsMessages(String verb, int actualCount, @Nullable String parameterAdjective,
68+
@Nullable String reason) {
69+
int totalCount = this.arguments.getTotalLength();
70+
return "Configuration error: @%s %s %s %s%s%s but there %s %s %s provided.%nNote: the provided arguments were %s".formatted(
71+
this.declarationContext.getAnnotationName(), verb, actualCount,
72+
parameterAdjective == null ? "" : parameterAdjective + " ",
73+
pluralize(actualCount, "parameter", "parameters"), reason == null ? "" : " (due to %s)".formatted(reason),
74+
pluralize(totalCount, "was", "were"), totalCount, pluralize(totalCount, "argument", "arguments"),
75+
Arrays.toString(this.arguments.getAllPayloads()));
76+
}
77+
5978
private ArgumentCountValidationMode getArgumentCountValidationMode(ExtensionContext extensionContext) {
6079
ArgumentCountValidationMode mode = declarationContext.getArgumentCountValidationMode();
6180
if (mode != ArgumentCountValidationMode.DEFAULT) {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ public ResolverFacade getResolverFacade() {
130130
@Override
131131
public ParameterizedClassInvocationContext createInvocationContext(ParameterizedInvocationNameFormatter formatter,
132132
Arguments arguments, int invocationIndex) {
133+
133134
return new ParameterizedClassInvocationContext(this, formatter, arguments, invocationIndex);
134135
}
135136

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,9 @@ static ResolverFacade create(Class<?> clazz, List<Field> fields) {
9898
Stream.concat(uniqueIndexedParameters.values().stream(), aggregatorParameters.stream()) //
9999
.forEach(declaration -> makeAccessible(declaration.getField()));
100100

101-
return new ResolverFacade(clazz, uniqueIndexedParameters, aggregatorParameters, 0);
101+
var requiredParameterCount = new RequiredParameterCount(uniqueIndexedParameters.size(), "field injection");
102+
103+
return new ResolverFacade(clazz, uniqueIndexedParameters, aggregatorParameters, 0, requiredParameterCount);
102104
}
103105

104106
static ResolverFacade create(Constructor<?> constructor, ParameterizedClass annotation) {
@@ -155,27 +157,35 @@ else if (aggregatorParameters.isEmpty()) {
155157
}
156158
}
157159
return new ResolverFacade(executable, indexedParameters, new LinkedHashSet<>(aggregatorParameters.values()),
158-
indexOffset);
160+
indexOffset, null);
159161
}
160162

161163
private final int parameterIndexOffset;
162164
private final Map<ParameterDeclaration, Resolver> resolvers;
163165
private final DefaultParameterDeclarations indexedParameterDeclarations;
164166
private final Set<? extends ResolvableParameterDeclaration> aggregatorParameters;
167+
private final @Nullable RequiredParameterCount requiredParameterCount;
165168

166169
private ResolverFacade(AnnotatedElement sourceElement,
167170
NavigableMap<Integer, ? extends ResolvableParameterDeclaration> indexedParameters,
168-
Set<? extends ResolvableParameterDeclaration> aggregatorParameters, int parameterIndexOffset) {
171+
Set<? extends ResolvableParameterDeclaration> aggregatorParameters, int parameterIndexOffset,
172+
@Nullable RequiredParameterCount requiredParameterCount) {
169173
this.aggregatorParameters = aggregatorParameters;
170174
this.parameterIndexOffset = parameterIndexOffset;
171175
this.resolvers = new ConcurrentHashMap<>(indexedParameters.size() + aggregatorParameters.size());
172176
this.indexedParameterDeclarations = new DefaultParameterDeclarations(sourceElement, indexedParameters);
177+
this.requiredParameterCount = requiredParameterCount;
173178
}
174179

175180
ParameterDeclarations getIndexedParameterDeclarations() {
176181
return this.indexedParameterDeclarations;
177182
}
178183

184+
@Nullable
185+
RequiredParameterCount getRequiredParameterCount() {
186+
return this.requiredParameterCount;
187+
}
188+
179189
boolean isSupportedParameter(ParameterContext parameterContext, EvaluatedArgumentSet arguments) {
180190
int index = toLogicalIndex(parameterContext);
181191
if (this.indexedParameterDeclarations.get(index).isPresent()) {
@@ -495,6 +505,7 @@ private record Converter(ArgumentConverter argumentConverter) implements Resolve
495505
@Override
496506
public @Nullable Object resolve(FieldContext fieldContext, ExtensionContext extensionContext,
497507
EvaluatedArgumentSet arguments, int invocationIndex) {
508+
498509
Object argument = arguments.getConsumedPayload(fieldContext.getParameterIndex());
499510
try {
500511
return this.argumentConverter.convert(argument, fieldContext);
@@ -752,4 +763,7 @@ public boolean supports(ParameterContext parameterContext) {
752763
invocationIndex, Optional.of(parameterContext)));
753764
}
754765
}
766+
767+
record RequiredParameterCount(int value, String reason) {
768+
}
755769
}

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

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,17 @@ void declaredIndexMustBeUnique() {
553553
"Configuration error: duplicate index declared in @Parameter(0) annotation on fields [int %s.i, long %s.l].".formatted(
554554
classTemplateClass.getName(), classTemplateClass.getName()))));
555555
}
556+
557+
@Test
558+
void failsWithMeaningfulErrorWhenTooFewArgumentsProvidedForFieldInjection() {
559+
var results = executeTestsForClass(NotEnoughArgumentsForFieldsTestCase.class);
560+
561+
results.containerEvents().assertThatEvents() //
562+
.haveExactly(1, finishedWithFailure(message(withPlatformSpecificLineSeparator(
563+
"""
564+
Configuration error: @ParameterizedClass has 2 required parameters (due to field injection) but there was 1 argument provided.
565+
Note: the provided arguments were [1]"""))));
566+
}
556567
}
557568

558569
@Nested
@@ -727,12 +738,12 @@ void failsForLifecycleMethodWithInvalidParameters() {
727738

728739
var results = executeTestsForClass(LifecycleMethodWithInvalidParametersTestCase.class);
729740

730-
var expectedMessage = """
731-
2 configuration errors:
732-
- parameter 'value' with index 0 is incompatible with the parameter declared on the parameterized class: \
733-
expected type 'int' but found 'long'
734-
- parameter 'anotherValue' with index 1 must not be annotated with @ConvertWith"""//
735-
.replace("\n", System.lineSeparator()); // use platform-specific line separators
741+
var expectedMessage = withPlatformSpecificLineSeparator(
742+
"""
743+
2 configuration errors:
744+
- parameter 'value' with index 0 is incompatible with the parameter declared on the parameterized class: \
745+
expected type 'int' but found 'long'
746+
- parameter 'anotherValue' with index 1 must not be annotated with @ConvertWith""");
736747

737748
var failedResult = getFirstTestExecutionResult(results.containerEvents().failed());
738749
assertThat(failedResult.getThrowable().orElseThrow()) //
@@ -795,6 +806,10 @@ void lifecycleMethodsMustNotBeDeclaredInRegularTestClasses() {
795806
}
796807
}
797808

809+
private static String withPlatformSpecificLineSeparator(String textBlock) {
810+
return textBlock.replace("\n", System.lineSeparator());
811+
}
812+
798813
// -------------------------------------------------------------------
799814

800815
private static Stream<String> invocationDisplayNames(EngineExecutionResults results) {
@@ -1695,6 +1710,22 @@ void test() {
16951710
}
16961711
}
16971712

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

0 commit comments

Comments
 (0)