Skip to content
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

Refactor test template name handling #603

Merged
merged 8 commits into from
Mar 10, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 10 additions & 1 deletion docs/retrying-test.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,18 @@ The default for `minSuccess` is 1. The value must be greater than or equal to 1.

=== onExceptions [optional]

By default a test annotated with `@RetryingTest` will be retried on all exceptions except https://ota4j-team.github.io/opentest4j/docs/current/api/org/opentest4j/TestAbortedException.html[`TestAbortedException`] (which will abort the test entirely).
By default, a test annotated with `@RetryingTest` will be retried on all exceptions except https://ota4j-team.github.io/opentest4j/docs/current/api/org/opentest4j/TestAbortedException.html[`TestAbortedException`] (which will abort the test entirely).
To only retry on specific exceptions, use `onExceptions`.

=== name [optional]

The `name` attribute specifies the display name for the individual test invocations.
To give a custom name to your container, use `@DisplayName`.
The `name` attribute supports `{index}`, it will get replaced by the current invocation index.
The `name` attribute supports `{displayName}`, it will get replaced by the test container display name.

The default for `name` is `[{index}]`.

== Basic Use

`@RetryingTest(n)` is used _instead_ of `@Test` or other such annotations (e.g. `@RepeatedTest`).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,38 @@
* http://www.eclipse.org/legal/epl-v20.html
*/

package org.junitpioneer.jupiter.cartesian;
package org.junitpioneer.internal;

import static java.util.stream.Collectors.joining;
import static org.junitpioneer.jupiter.cartesian.CartesianTest.ARGUMENTS_PLACEHOLDER;
import static org.junitpioneer.jupiter.cartesian.CartesianTest.DISPLAY_NAME_PLACEHOLDER;
import static org.junitpioneer.jupiter.cartesian.CartesianTest.INDEX_PLACEHOLDER;

import java.text.MessageFormat;
import java.util.Arrays;
import java.util.stream.IntStream;

import org.junit.jupiter.api.extension.ExtensionConfigurationException;
import org.junitpioneer.internal.PioneerUtils;

class CartesianTestNameFormatter {
public class TestNameFormatter {

private final String pattern;
private final String displayName;
private final Class<?> forClass;

CartesianTestNameFormatter(String pattern, String displayName) {
public static final String DISPLAY_NAME_PLACEHOLDER = "{displayName}";
public static final String INDEX_PLACEHOLDER = "{index}";
public static final String ARGUMENTS_PLACEHOLDER = "{arguments}";

public TestNameFormatter(String pattern, String displayName, Class<?> forClass) {
this.pattern = pattern;
this.displayName = displayName;
this.forClass = forClass;
}

String format(int invocationIndex, Object... arguments) {
public String format(int invocationIndex, Object... arguments) {
try {
return formatSafely(invocationIndex, arguments);
}
catch (Exception ex) {
String message = "The display name pattern defined for the CartesianProductTest is invalid. "
String message = "The display name pattern defined for the " + forClass.getName() + " is invalid. "
+ "See nested exception for further details.";
throw new ExtensionConfigurationException(message, ex);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.junit.platform.commons.support.ReflectionSupport;
import org.junitpioneer.internal.PioneerAnnotationUtils;
import org.junitpioneer.internal.PioneerUtils;
import org.junitpioneer.internal.TestNameFormatter;

/**
* @deprecated Replaced by `org.junitpioneer.jupiter.cartesian.CartesianTestExtension`.
Expand All @@ -53,20 +54,20 @@ public boolean supportsTestTemplate(ExtensionContext context) {
@Override
public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(ExtensionContext context) {
List<List<?>> sets = computeSets(context);
CartesianProductTestNameFormatter formatter = createNameFormatter(context);
TestNameFormatter formatter = createNameFormatter(context);
return cartesianProduct(sets)
.stream()
.map(params -> new CartesianProductTestInvocationContext(params, formatter));
}

private CartesianProductTestNameFormatter createNameFormatter(ExtensionContext context) {
private TestNameFormatter createNameFormatter(ExtensionContext context) {
CartesianProductTest annotation = findAnnotation(context.getRequiredTestMethod(), CartesianProductTest.class)
.orElseThrow(() -> new ExtensionConfigurationException("@CartesianProductTest not found."));
String pattern = annotation.name();
if (pattern.isEmpty())
throw new ExtensionConfigurationException("CartesianProductTest can not have a non-empty display name.");
String displayName = context.getDisplayName();
return new CartesianProductTestNameFormatter(pattern, displayName);
return new TestNameFormatter(pattern, displayName, CartesianProductTest.class);
}

private List<List<?>> computeSets(ExtensionContext context) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
import org.junitpioneer.internal.TestNameFormatter;

/**
* @deprecated scheduled to be removed in 2.0
Expand All @@ -23,9 +24,9 @@
class CartesianProductTestInvocationContext implements TestTemplateInvocationContext {

private final List<?> parameters;
private final CartesianProductTestNameFormatter formatter;
private final TestNameFormatter formatter;

CartesianProductTestInvocationContext(List<?> parameters, CartesianProductTestNameFormatter formatter) {
CartesianProductTestInvocationContext(List<?> parameters, TestNameFormatter formatter) {
this.parameters = parameters;
this.formatter = formatter;
}
Expand Down

This file was deleted.

42 changes: 42 additions & 0 deletions src/main/java/org/junitpioneer/jupiter/RetryingTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.parallel.Execution;
import org.junitpioneer.internal.TestNameFormatter;

/**
* {@code @RetryingTest} is a JUnit Jupiter extension that retries
Expand Down Expand Up @@ -78,6 +79,47 @@
@TestTemplate
public @interface RetryingTest {

/**
* Placeholder for the display name of a {@code @RetryingTest}:
* <code>{displayName}</code>
*
* @since ?
* @see #name
*/
String DISPLAY_NAME_PLACEHOLDER = TestNameFormatter.DISPLAY_NAME_PLACEHOLDER;

/**
* Placeholder for the current invocation index of a {@code @RetryingTest}
* method (1-based): <code>{index}</code>
*
* @since ?
* @see #name
*/
String INDEX_PLACEHOLDER = TestNameFormatter.INDEX_PLACEHOLDER;

/**
* <p>The display name to be used for individual invocations of the
* parameterized test; never blank or consisting solely of whitespace.
* </p>
*
* <p>Defaults to [{index}] {arguments}.
* </p>
* <p>
* Supported placeholders:
* <p>
* - {@link org.junitpioneer.jupiter.RetryingTest#DISPLAY_NAME_PLACEHOLDER}
* - {@link org.junitpioneer.jupiter.RetryingTest#INDEX_PLACEHOLDER}
*
* <p>You may use {@link java.text.MessageFormat} patterns
* to customize formatting.
* </p>
*
* @since ?
* @see java.text.MessageFormat
* @see org.junit.jupiter.params.ParameterizedTest#name()
*/
String name() default "[{index}]";

/**
* Specifies how often the test is executed at most.
*
Expand Down
20 changes: 15 additions & 5 deletions src/main/java/org/junitpioneer/jupiter/RetryingTestExtension.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@
import java.util.NoSuchElementException;
import java.util.stream.Stream;

import org.junit.jupiter.api.extension.ExtensionConfigurationException;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.TestExecutionExceptionHandler;
import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider;
import org.junit.platform.commons.support.AnnotationSupport;
import org.junitpioneer.internal.PioneerAnnotationUtils;
import org.junitpioneer.internal.TestNameFormatter;
import org.opentest4j.AssertionFailedError;
import org.opentest4j.TestAbortedException;

Expand Down Expand Up @@ -63,7 +65,7 @@ private static FailedTestRetrier retrierFor(ExtensionContext context) {
Method test = context.getRequiredTestMethod();
return context
.getStore(NAMESPACE)
.getOrComputeIfAbsent(test.toString(), __ -> FailedTestRetrier.createFor(test),
.getOrComputeIfAbsent(test.toString(), __ -> FailedTestRetrier.createFor(test, context),
FailedTestRetrier.class);
}

Expand All @@ -72,27 +74,31 @@ private static class FailedTestRetrier implements Iterator<RetryingTestInvocatio
private final int maxRetries;
private final int minSuccess;
private final Class<? extends Throwable>[] expectedExceptions;
private final TestNameFormatter formatter;

private int retriesSoFar;
private int exceptionsSoFar;
private boolean seenFailedAssumption;
private boolean seenUnexpectedException;

private FailedTestRetrier(int maxRetries, int minSuccess, Class<? extends Throwable>[] expectedExceptions) {
private FailedTestRetrier(int maxRetries, int minSuccess, Class<? extends Throwable>[] expectedExceptions,
TestNameFormatter formatter) {
this.maxRetries = maxRetries;
this.minSuccess = minSuccess;
this.expectedExceptions = expectedExceptions;
this.retriesSoFar = 0;
this.exceptionsSoFar = 0;
this.formatter = formatter;
}

static FailedTestRetrier createFor(Method test) {
static FailedTestRetrier createFor(Method test, ExtensionContext context) {
RetryingTest retryingTest = AnnotationSupport
.findAnnotation(test, RetryingTest.class)
.orElseThrow(() -> new IllegalStateException("@RetryingTest is missing."));

int maxAttempts = retryingTest.maxAttempts() != 0 ? retryingTest.maxAttempts() : retryingTest.value();
int minSuccess = retryingTest.minSuccess();
String pattern = retryingTest.name();

if (maxAttempts == 0)
throw new IllegalStateException("@RetryingTest requires that one of `value` or `maxAttempts` be set.");
Expand All @@ -111,8 +117,12 @@ else if (maxAttempts <= minSuccess) {
format("@RetryingTest requires that `maxAttempts` be greater than %s.%s",
minSuccess == 1 ? "1" : "`minSuccess`", additionalMessage));
}
if (pattern.isEmpty())
throw new ExtensionConfigurationException("RetryingTest can not have an empty display name.");
String displayName = context.getDisplayName();
TestNameFormatter formatter = new TestNameFormatter(pattern, displayName, RetryingTest.class);

return new FailedTestRetrier(maxAttempts, minSuccess, retryingTest.onExceptions());
return new FailedTestRetrier(maxAttempts, minSuccess, retryingTest.onExceptions(), formatter);
}

void failed(Throwable exception) throws Throwable {
Expand Down Expand Up @@ -167,7 +177,7 @@ public RetryingTestInvocationContext next() {
if (!hasNext())
throw new NoSuchElementException();
retriesSoFar++;
return new RetryingTestInvocationContext();
return new RetryingTestInvocationContext(formatter);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,19 @@
package org.junitpioneer.jupiter;

import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
import org.junitpioneer.internal.TestNameFormatter;

class RetryingTestInvocationContext implements TestTemplateInvocationContext {

private final TestNameFormatter formatter;

RetryingTestInvocationContext(TestNameFormatter formatter) {
this.formatter = formatter;
}

@Override
public String getDisplayName(int invocationIndex) {
return formatter.format(invocationIndex);
}

}