Skip to content
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 @@ -54,6 +54,8 @@ repository on GitHub.

* The `@CsvSource` and `@CsvFileSource` annotations now allow specifying
a custom comment character using the new `commentCharacter` attribute.
* Improve error message when using `@ParameterizedClass` with field injection and not
providing enough arguments.


[[release-notes-6.0.1-junit-vintage]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.util.Arrays;
import java.util.Optional;

import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.extension.ExtensionConfigurationException;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
Expand All @@ -36,6 +37,7 @@ class ArgumentCountValidator {
}

void validate(ExtensionContext extensionContext) {
validateRequiredArgumentsArePresent();
ArgumentCountValidationMode argumentCountValidationMode = getArgumentCountValidationMode(extensionContext);
switch (argumentCountValidationMode) {
case DEFAULT, NONE -> {
Expand All @@ -45,17 +47,34 @@ void validate(ExtensionContext extensionContext) {
this.arguments);
int totalCount = this.arguments.getTotalLength();
Preconditions.condition(consumedCount == totalCount,
() -> "Configuration error: @%s consumes %s %s but there %s %s %s provided.%nNote: the provided arguments were %s".formatted(
this.declarationContext.getAnnotationName(), consumedCount,
pluralize(consumedCount, "parameter", "parameters"), pluralize(totalCount, "was", "were"),
totalCount, pluralize(totalCount, "argument", "arguments"),
Arrays.toString(this.arguments.getAllPayloads())));
() -> wrongNumberOfArgumentsMessages("consumes", consumedCount, null, null));
}
default -> throw new ExtensionConfigurationException(
"Unsupported argument count validation mode: " + argumentCountValidationMode);
}
}

private void validateRequiredArgumentsArePresent() {
var requiredParameterCount = this.declarationContext.getResolverFacade().getRequiredParameterCount();
if (requiredParameterCount != null) {
var totalCount = this.arguments.getTotalLength();
Preconditions.condition(requiredParameterCount.value() <= totalCount,
() -> wrongNumberOfArgumentsMessages("has", requiredParameterCount.value(), "required",
requiredParameterCount.reason()));
}
}

private String wrongNumberOfArgumentsMessages(String verb, int actualCount, @Nullable String parameterAdjective,
@Nullable String reason) {
int totalCount = this.arguments.getTotalLength();
return "Configuration error: @%s %s %s %s%s%s but there %s %s %s provided.%nNote: the provided arguments were %s".formatted(
this.declarationContext.getAnnotationName(), verb, actualCount,
parameterAdjective == null ? "" : parameterAdjective + " ",
pluralize(actualCount, "parameter", "parameters"), reason == null ? "" : " (due to %s)".formatted(reason),
pluralize(totalCount, "was", "were"), totalCount, pluralize(totalCount, "argument", "arguments"),
Arrays.toString(this.arguments.getAllPayloads()));
}

private ArgumentCountValidationMode getArgumentCountValidationMode(ExtensionContext extensionContext) {
ArgumentCountValidationMode mode = declarationContext.getArgumentCountValidationMode();
if (mode != ArgumentCountValidationMode.DEFAULT) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ public ResolverFacade getResolverFacade() {
@Override
public ParameterizedClassInvocationContext createInvocationContext(ParameterizedInvocationNameFormatter formatter,
Arguments arguments, int invocationIndex) {

return new ParameterizedClassInvocationContext(this, formatter, arguments, invocationIndex);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ static ResolverFacade create(Class<?> clazz, List<Field> fields) {
Stream.concat(uniqueIndexedParameters.values().stream(), aggregatorParameters.stream()) //
.forEach(declaration -> makeAccessible(declaration.getField()));

return new ResolverFacade(clazz, uniqueIndexedParameters, aggregatorParameters, 0);
var requiredParameterCount = new RequiredParameterCount(uniqueIndexedParameters.size(), "field injection");

return new ResolverFacade(clazz, uniqueIndexedParameters, aggregatorParameters, 0, requiredParameterCount);
}

static ResolverFacade create(Constructor<?> constructor, ParameterizedClass annotation) {
Expand Down Expand Up @@ -155,27 +157,35 @@ else if (aggregatorParameters.isEmpty()) {
}
}
return new ResolverFacade(executable, indexedParameters, new LinkedHashSet<>(aggregatorParameters.values()),
indexOffset);
indexOffset, null);
}

private final int parameterIndexOffset;
private final Map<ParameterDeclaration, Resolver> resolvers;
private final DefaultParameterDeclarations indexedParameterDeclarations;
private final Set<? extends ResolvableParameterDeclaration> aggregatorParameters;
private final @Nullable RequiredParameterCount requiredParameterCount;

private ResolverFacade(AnnotatedElement sourceElement,
NavigableMap<Integer, ? extends ResolvableParameterDeclaration> indexedParameters,
Set<? extends ResolvableParameterDeclaration> aggregatorParameters, int parameterIndexOffset) {
Set<? extends ResolvableParameterDeclaration> aggregatorParameters, int parameterIndexOffset,
@Nullable RequiredParameterCount requiredParameterCount) {
this.aggregatorParameters = aggregatorParameters;
this.parameterIndexOffset = parameterIndexOffset;
this.resolvers = new ConcurrentHashMap<>(indexedParameters.size() + aggregatorParameters.size());
this.indexedParameterDeclarations = new DefaultParameterDeclarations(sourceElement, indexedParameters);
this.requiredParameterCount = requiredParameterCount;
}

ParameterDeclarations getIndexedParameterDeclarations() {
return this.indexedParameterDeclarations;
}

@Nullable
RequiredParameterCount getRequiredParameterCount() {
return this.requiredParameterCount;
}

boolean isSupportedParameter(ParameterContext parameterContext, EvaluatedArgumentSet arguments) {
int index = toLogicalIndex(parameterContext);
if (this.indexedParameterDeclarations.get(index).isPresent()) {
Expand Down Expand Up @@ -495,6 +505,7 @@ private record Converter(ArgumentConverter argumentConverter) implements Resolve
@Override
public @Nullable Object resolve(FieldContext fieldContext, ExtensionContext extensionContext,
EvaluatedArgumentSet arguments, int invocationIndex) {

Object argument = arguments.getConsumedPayload(fieldContext.getParameterIndex());
try {
return this.argumentConverter.convert(argument, fieldContext);
Expand Down Expand Up @@ -752,4 +763,7 @@ public boolean supports(ParameterContext parameterContext) {
invocationIndex, Optional.of(parameterContext)));
}
}

record RequiredParameterCount(int value, String reason) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,17 @@ void declaredIndexMustBeUnique() {
"Configuration error: duplicate index declared in @Parameter(0) annotation on fields [int %s.i, long %s.l].".formatted(
classTemplateClass.getName(), classTemplateClass.getName()))));
}

@Test
void failsWithMeaningfulErrorWhenTooFewArgumentsProvidedForFieldInjection() {
var results = executeTestsForClass(NotEnoughArgumentsForFieldsTestCase.class);

results.containerEvents().assertThatEvents() //
.haveExactly(1, finishedWithFailure(message(withPlatformSpecificLineSeparator(
"""
Configuration error: @ParameterizedClass has 2 required parameters (due to field injection) but there was 1 argument provided.
Note: the provided arguments were [1]"""))));
}
}

@Nested
Expand Down Expand Up @@ -726,12 +737,12 @@ void failsForLifecycleMethodWithInvalidParameters() {

var results = executeTestsForClass(LifecycleMethodWithInvalidParametersTestCase.class);

var expectedMessage = """
2 configuration errors:
- parameter 'value' with index 0 is incompatible with the parameter declared on the parameterized class: \
expected type 'int' but found 'long'
- parameter 'anotherValue' with index 1 must not be annotated with @ConvertWith"""//
.replace("\n", System.lineSeparator()); // use platform-specific line separators
var expectedMessage = withPlatformSpecificLineSeparator(
"""
2 configuration errors:
- parameter 'value' with index 0 is incompatible with the parameter declared on the parameterized class: \
expected type 'int' but found 'long'
- parameter 'anotherValue' with index 1 must not be annotated with @ConvertWith""");

var failedResult = getFirstTestExecutionResult(results.containerEvents().failed());
assertThat(failedResult.getThrowable().orElseThrow()) //
Expand Down Expand Up @@ -794,6 +805,10 @@ void lifecycleMethodsMustNotBeDeclaredInRegularTestClasses() {
}
}

private static String withPlatformSpecificLineSeparator(String textBlock) {
return textBlock.replace("\n", System.lineSeparator());
}

// -------------------------------------------------------------------

private static Stream<String> invocationDisplayNames(EngineExecutionResults results) {
Expand Down Expand Up @@ -1693,6 +1708,22 @@ void test() {
}
}

@ParameterizedClass
@ValueSource(ints = 1)
static class NotEnoughArgumentsForFieldsTestCase {

@Parameter(0)
int i;

@Parameter(1)
String s;

@org.junit.jupiter.api.Test
void test() {
fail("should not be called");
}
}

@ParameterizedClass
@CsvSource({ "unused1, foo, unused2, bar", "unused4, baz, unused5, qux" })
static class InvalidUnusedParameterIndexesTestCase {
Expand Down