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 @@ -24,14 +24,17 @@
import io.qameta.allure.model.Status;
import io.qameta.allure.util.ParameterUtils;
import io.qameta.allure.util.ResultsUtils;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.InvocationInterceptor;
import org.junit.jupiter.api.extension.ReflectiveInvocationContext;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Set;

/**
* Reports JUnit Jupiter fixture and parameter execution details to Allure.
Expand All @@ -42,17 +45,23 @@
* Platform unique ids — see {@link AllureJunitPlatform#scopeKey(String)} and
* {@link AllureJunitPlatform#testKey(String)}.</p>
*/
public class AllureJupiter implements InvocationInterceptor {
public class AllureJupiter implements InvocationInterceptor, BeforeEachCallback {

private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create(AllureJupiter.class);

private static final String TEST = "test";
private static final String TEMPLATE = "template";
private static final String PARAMETERS = "parameters";
private static final String PREPARE = "prepare";
private static final String TEAR_DOWN = "tear_down";
private static final String CAPTURED_PARAMETERS = "captured_parameters";

// parameterized class support requires the ParameterInfo API of junit-jupiter-params 6.x
private static final boolean CLASS_PARAMETERS_SUPPORTED = isClassAvailableOnClasspath("org.junit.jupiter.params.ParameterInfo");
private static final boolean CURRENT_PARAMETER_INFO_SUPPORTED = isClassAvailableOnClasspath(
"org.junit.jupiter.params.ParameterInfo"
);
private static final boolean LEGACY_PARAMETER_INFO_SUPPORTED = isClassAvailableOnClasspath(
"org.junit.jupiter.params.support.ParameterInfo"
);

/**
* Returns the lifecycle. Resolved at call time, so the extension follows process-wide lifecycle swaps.
Expand All @@ -63,6 +72,24 @@ protected AllureLifecycle getLifecycle() {
return Allure.getLifecycle();
}

/**
* {@inheritDoc}
*/
@Override
public void beforeEach(final ExtensionContext extensionContext) {
final Method testMethod = extensionContext.getRequiredTestMethod();
if (!shouldHandle(extensionContext, PARAMETERS, testMethod)) {
return;
}
final List<Parameter> parameters = getParameters(extensionContext);
if (parameters.isEmpty()) {
return;
}
extensionContext.getStore(NAMESPACE)
.put(CAPTURED_PARAMETERS, new ParameterCapture(parameters));
addParameters(extensionContext, parameters);
}

/**
* {@inheritDoc}
*/
Expand All @@ -75,7 +102,7 @@ public void interceptTestMethod(final Invocation<Void> invocation,
invocation.proceed();
return;
}
addParameters(extensionContext, getClassParameters(invocationContext, extensionContext));
replaceCapturedParameters(extensionContext, getClassParameters(invocationContext, extensionContext));
invocation.proceed();
}

Expand All @@ -93,7 +120,7 @@ public void interceptTestTemplateMethod(final Invocation<Void> invocation,
}
final List<Parameter> testParameters = new ArrayList<>(getClassParameters(invocationContext, extensionContext));
testParameters.addAll(getArgumentParameters(invocationContext));
addParameters(extensionContext, testParameters);
replaceCapturedParameters(extensionContext, testParameters);
invocation.proceed();
}

Expand All @@ -108,15 +135,53 @@ private void addParameters(final ExtensionContext extensionContext,
);
}

private void replaceCapturedParameters(final ExtensionContext extensionContext,
final List<Parameter> testParameters) {
final ParameterCapture capture = extensionContext.getStore(NAMESPACE)
.remove(CAPTURED_PARAMETERS, ParameterCapture.class);
if (capture == null) {
addParameters(extensionContext, testParameters);
return;
}

// ParameterInfo exposes source arguments before setup. Once the invocation exists, prefer its converted
// values and remove only the objects captured here so parameters added by user code remain untouched.
final Set<Parameter> capturedParameters = Collections.newSetFromMap(new IdentityHashMap<>());
capturedParameters.addAll(capture.parameters());
getLifecycle().updateTest(
AllureJunitPlatform.testKey(extensionContext.getUniqueId()),
testResult -> {
testResult.getParameters().removeIf(capturedParameters::contains);
testResult.getParameters().addAll(testParameters);
}
);
}

private List<Parameter> getParameters(final ExtensionContext extensionContext) {
if (CURRENT_PARAMETER_INFO_SUPPORTED) {
return AllureJupiterParameterInfoSupport.getParameters(extensionContext);
}
if (LEGACY_PARAMETER_INFO_SUPPORTED) {
return AllureJupiterLegacyParameterInfoSupport.getParameters(extensionContext);
}
return Collections.emptyList();
}

private List<Parameter> getClassParameters(final ReflectiveInvocationContext<Method> invocationContext,
final ExtensionContext extensionContext) {
if (!CLASS_PARAMETERS_SUPPORTED) {
return Collections.emptyList();
if (CURRENT_PARAMETER_INFO_SUPPORTED) {
return AllureJupiterParameterInfoSupport.getClassParameters(
extensionContext,
invocationContext.getExecutable()
);
}
return AllureJupiterParameterInfoSupport.getClassParameters(
extensionContext,
invocationContext.getExecutable()
);
if (LEGACY_PARAMETER_INFO_SUPPORTED) {
return AllureJupiterLegacyParameterInfoSupport.getClassParameters(
extensionContext,
invocationContext.getExecutable()
);
}
return Collections.emptyList();
}

private List<Parameter> getArgumentParameters(final ReflectiveInvocationContext<Method> invocationContext) {
Expand Down Expand Up @@ -258,4 +323,7 @@ private boolean shouldHandle(final ExtensionContext extensionContext,
.getOrComputeIfAbsent(key, ignored -> marker);
return marker.equals(storedMarker);
}

private record ParameterCapture(List<Parameter> parameters) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright 2016-2026 Qameta Software Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.qameta.allure.jupiter;

import io.qameta.allure.model.Parameter;
import io.qameta.allure.util.ParameterUtils;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
import org.junit.jupiter.params.support.ParameterDeclaration;
import org.junit.jupiter.params.support.ParameterInfo;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;

/**
* Reads the legacy JUnit Jupiter {@link ParameterInfo} API to report arguments with junit-jupiter-params 5.13. The
* API is deprecated in JUnit 6 — every reference to it stays inside this class, which is loaded only after a
* classpath check and only when the replacement API is unavailable.
*/
@SuppressWarnings({"deprecation", "removal"})
/* package-private */ final class AllureJupiterLegacyParameterInfoSupport {

private AllureJupiterLegacyParameterInfoSupport() {
throw new IllegalStateException("do not instance");
}

/**
* Collects all arguments available for the current parameterized invocation as Allure parameters.
*
* @param extensionContext the extension context of the running test
* @return the Allure parameters, empty when the test is not parameterized
*/
static List<Parameter> getParameters(final ExtensionContext extensionContext) {
return collectParameters(extensionContext, info -> true);
}

/**
* Collects the arguments of every enclosing parameterized class invocation as Allure parameters, outermost
* first. The test method's own arguments are excluded: they are reported from the invocation context.
*
* @param extensionContext the extension context of the running test
* @param testMethod the running test method
* @return the class-level Allure parameters, empty when the test runs outside parameterized classes
*/
static List<Parameter> getClassParameters(final ExtensionContext extensionContext,
final Method testMethod) {
return collectParameters(
extensionContext,
info -> !testMethod.equals(info.getDeclarations().getSourceElement())
);
}

private static List<Parameter> collectParameters(final ExtensionContext extensionContext,
final Predicate<ParameterInfo> filter) {
final List<Parameter> result = new ArrayList<>();
final Set<ParameterInfo> visited = Collections.newSetFromMap(new IdentityHashMap<>());
Optional<ExtensionContext> current = Optional.of(extensionContext);
while (current.isPresent()) {
final ParameterInfo info = ParameterInfo.get(current.get());
if (Objects.isNull(info)) {
break;
}
if (visited.add(info) && filter.test(info)) {
// contexts are walked inner to outer: prepend so outer class arguments come first
result.addAll(0, toParameters(info));
}
current = current.get().getParent();
}
return result;
}

private static List<Parameter> toParameters(final ParameterInfo info) {
final ArgumentsAccessor arguments = info.getArguments();
final List<Parameter> result = new ArrayList<>();
for (final ParameterDeclaration declaration : info.getDeclarations().getAll()) {
final int index = declaration.getParameterIndex();
if (index < 0 || index >= arguments.size()) {
continue;
}
result.add(
ParameterUtils.createParameter(
declaration.getAnnotatedElement(),
arguments.get(index),
declaration.getParameterName().orElse("arg" + index)
)
);
}
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,29 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;

/**
* Reads the JUnit Jupiter {@link ParameterInfo} API to report parameterized class arguments. The API exists since
* junit-jupiter-params 6.0 — every reference to it stays inside this class, which is loaded only after a classpath
* check.
* Reads the JUnit Jupiter {@link ParameterInfo} API to report parameterized invocation arguments. The API exists
* since junit-jupiter-params 6.0 — every reference to it stays inside this class, which is loaded only after a
* classpath check.
*/
/* package-private */ final class AllureJupiterParameterInfoSupport {

private AllureJupiterParameterInfoSupport() {
throw new IllegalStateException("do not instance");
}

/**
* Collects all arguments available for the current parameterized invocation as Allure parameters.
*
* @param extensionContext the extension context of the running test
* @return the Allure parameters, empty when the test is not parameterized
*/
static List<Parameter> getParameters(final ExtensionContext extensionContext) {
return collectParameters(extensionContext, info -> true);
}

/**
* Collects the arguments of every enclosing parameterized class invocation as Allure parameters, outermost
* first. The test method's own arguments are excluded: they are reported from the invocation context.
Expand All @@ -52,6 +63,14 @@ private AllureJupiterParameterInfoSupport() {
*/
static List<Parameter> getClassParameters(final ExtensionContext extensionContext,
final Method testMethod) {
return collectParameters(
extensionContext,
info -> !testMethod.equals(info.getDeclarations().getSourceElement())
);
}

private static List<Parameter> collectParameters(final ExtensionContext extensionContext,
final Predicate<ParameterInfo> filter) {
final List<Parameter> result = new ArrayList<>();
final Set<ParameterInfo> visited = Collections.newSetFromMap(new IdentityHashMap<>());
Optional<ExtensionContext> current = Optional.of(extensionContext);
Expand All @@ -60,7 +79,7 @@ static List<Parameter> getClassParameters(final ExtensionContext extensionContex
if (Objects.isNull(info)) {
break;
}
if (visited.add(info) && !testMethod.equals(info.getDeclarations().getSourceElement())) {
if (visited.add(info) && filter.test(info)) {
// contexts are walked inner to outer: prepend so outer class arguments come first
result.addAll(0, toParameters(info));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.qameta.allure.jupiter.features.GlobalErrorTests;
import io.qameta.allure.jupiter.features.NestedTemplatesTests;
import io.qameta.allure.jupiter.features.ParamAnnotationTests;
import io.qameta.allure.jupiter.features.ParameterizedBeforeEachTests;
import io.qameta.allure.jupiter.features.ParameterizedClassTests;
import io.qameta.allure.model.FixtureResult;
import io.qameta.allure.model.GlobalError;
Expand Down Expand Up @@ -135,6 +136,45 @@ void shouldReportFailedBeforeEachFixture() {
.containsExactly(testResult.getUuid());
}

@Test
@AllureFeatures.Fixtures
@AllureFeatures.Parameters
@AllureFeatures.History
void shouldKeepParametersWhenBeforeEachFails() {
final AllureResults failedResults;
ParameterizedBeforeEachTests.failBeforeEach(true);
try {
failedResults = runClasses(ParameterizedBeforeEachTests.class);
} finally {
ParameterizedBeforeEachTests.failBeforeEach(false);
}
final AllureResults passedResults = runClasses(ParameterizedBeforeEachTests.class);

assertThat(failedResults.getTestResults()).singleElement()
.extracting(TestResult::getStatus)
.isEqualTo(Status.BROKEN);
assertThat(passedResults.getTestResults()).singleElement()
.extracting(TestResult::getStatus)
.isEqualTo(Status.PASSED);

final TestResult failed = failedResults.getTestResults().get(0);
final TestResult passed = passedResults.getTestResults().get(0);
assertThat(failed.getParameters())
.extracting(Parameter::getName)
.containsExactlyInAnyOrder("UniqueId", "first", "second");
assertThat(failed.getParameters())
.filteredOn(parameter -> !"UniqueId".equals(parameter.getName()))
.extracting(Parameter::getName, Parameter::getValue)
.containsExactlyInAnyOrder(
tuple("first", "first value"),
tuple("second", "second value")
);
assertThat(failed.getParameters())
.containsExactlyInAnyOrderElementsOf(passed.getParameters());
assertThat(failed.getTestCaseId()).isEqualTo(passed.getTestCaseId());
assertThat(failed.getHistoryId()).isEqualTo(passed.getHistoryId());
}

@Test
void shouldReportGlobalErrorFromJupiterTest() {
final AllureResults results = runClasses(GlobalErrorTests.class);
Expand Down
Loading
Loading