
org.junitpioneer.jupiter.RepeatFailedTestExtension Maven / Gradle / Ivy
/*
* Copyright 2015-2020 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v20.html
*/
package org.junitpioneer.jupiter;
import static java.lang.String.format;
import static java.util.Spliterator.ORDERED;
import static java.util.Spliterators.spliteratorUnknownSize;
import static java.util.stream.StreamSupport.stream;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.stream.Stream;
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.opentest4j.TestAbortedException;
public class RepeatFailedTestExtension implements TestTemplateInvocationContextProvider, TestExecutionExceptionHandler {
private static final Namespace NAMESPACE = Namespace.create(RepeatFailedTestExtension.class);
@Override
public boolean supportsTestTemplate(ExtensionContext context) {
// the annotation only applies to methods (see its `@Target`),
// so it doesn't matter that this method checks meta-annotations
return PioneerAnnotationUtils.isAnyAnnotationPresent(context, RepeatFailedTest.class);
}
@Override
public Stream provideTestTemplateInvocationContexts(ExtensionContext context) {
FailedTestRepeater repeater = repeaterFor(context);
return stream(spliteratorUnknownSize(repeater, ORDERED), false);
}
@Override
public void handleTestExecutionException(ExtensionContext context, Throwable throwable) throws Throwable {
// this `context` (M) is a child of the context passed to `provideTestTemplateInvocationContexts` (T),
// which means M's store content is invisible to T's store; this can be fixed by using T's store here
ExtensionContext templateContext = context
.getParent()
.orElseThrow(() -> new IllegalStateException(
"Extension context \"" + context + "\" should have a parent context."));
repeaterFor(templateContext).failed(throwable);
}
private static FailedTestRepeater repeaterFor(ExtensionContext context) {
Method repeatedTest = context.getRequiredTestMethod();
return context
.getStore(NAMESPACE)
.getOrComputeIfAbsent(repeatedTest.toString(), __ -> FailedTestRepeater.createFor(repeatedTest),
FailedTestRepeater.class);
}
private static class FailedTestRepeater implements Iterator {
private final int maxRepetitions;
private int repetitionsSoFar;
private int exceptionsSoFar;
private FailedTestRepeater(int maxRepetitions) {
this.maxRepetitions = maxRepetitions;
this.repetitionsSoFar = 0;
this.exceptionsSoFar = 0;
}
static FailedTestRepeater createFor(Method repeatedTest) {
RepeatFailedTest repeatFailedTest = AnnotationSupport
.findAnnotation(repeatedTest, RepeatFailedTest.class)
.orElseThrow(() -> new IllegalStateException("@RepeatFailedTest is missing."));
return new FailedTestRepeater(repeatFailedTest.value());
}
void failed(Throwable exception) {
exceptionsSoFar++;
boolean allRepetitionsFailed = exceptionsSoFar == maxRepetitions;
if (allRepetitionsFailed)
throw new AssertionError(
format("Test execution #%d (of up to %d) failed ~> test fails - see cause for details",
exceptionsSoFar, maxRepetitions),
exception);
else
throw new TestAbortedException(
format("Test execution #%d (of up to %d) failed ~> will retry...", exceptionsSoFar, maxRepetitions),
exception);
}
@Override
public boolean hasNext() {
// there's always at least one execution
if (repetitionsSoFar == 0)
return true;
// if we caught an exception in each repetition, each repetition failed, including the previous one
boolean previousFailed = repetitionsSoFar == exceptionsSoFar;
boolean maxRepetitionsReached = repetitionsSoFar == maxRepetitions;
return previousFailed && !maxRepetitionsReached;
}
@Override
public RepeatFailedTestInvocationContext next() {
if (!hasNext())
throw new NoSuchElementException();
repetitionsSoFar++;
return new RepeatFailedTestInvocationContext();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy