From 04c2a34463b4311de2f8c3d97f08e67c2052bf57 Mon Sep 17 00:00:00 2001 From: Dmitry Baev Date: Fri, 4 Sep 2026 09:17:20 +0100 Subject: [PATCH] Preserve parameterized test identity when JUnit setup fails --- .../qameta/allure/jupiter/AllureJupiter.java | 90 ++++++++++++-- ...lureJupiterLegacyParameterInfoSupport.java | 110 ++++++++++++++++++ .../AllureJupiterParameterInfoSupport.java | 27 ++++- .../allure/jupiter/AllureJupiterTest.java | 40 +++++++ .../ParameterizedBeforeEachTests.java | 46 ++++++++ 5 files changed, 298 insertions(+), 15 deletions(-) create mode 100644 allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiterLegacyParameterInfoSupport.java create mode 100644 allure-jupiter/src/test/java/io/qameta/allure/jupiter/features/ParameterizedBeforeEachTests.java diff --git a/allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiter.java b/allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiter.java index 443780a40..5e2373a67 100644 --- a/allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiter.java +++ b/allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiter.java @@ -24,6 +24,7 @@ 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; @@ -31,7 +32,9 @@ 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. @@ -42,17 +45,23 @@ * Platform unique ids — see {@link AllureJunitPlatform#scopeKey(String)} and * {@link AllureJunitPlatform#testKey(String)}.

*/ -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. @@ -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 parameters = getParameters(extensionContext); + if (parameters.isEmpty()) { + return; + } + extensionContext.getStore(NAMESPACE) + .put(CAPTURED_PARAMETERS, new ParameterCapture(parameters)); + addParameters(extensionContext, parameters); + } + /** * {@inheritDoc} */ @@ -75,7 +102,7 @@ public void interceptTestMethod(final Invocation invocation, invocation.proceed(); return; } - addParameters(extensionContext, getClassParameters(invocationContext, extensionContext)); + replaceCapturedParameters(extensionContext, getClassParameters(invocationContext, extensionContext)); invocation.proceed(); } @@ -93,7 +120,7 @@ public void interceptTestTemplateMethod(final Invocation invocation, } final List testParameters = new ArrayList<>(getClassParameters(invocationContext, extensionContext)); testParameters.addAll(getArgumentParameters(invocationContext)); - addParameters(extensionContext, testParameters); + replaceCapturedParameters(extensionContext, testParameters); invocation.proceed(); } @@ -108,15 +135,53 @@ private void addParameters(final ExtensionContext extensionContext, ); } + private void replaceCapturedParameters(final ExtensionContext extensionContext, + final List 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 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 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 getClassParameters(final ReflectiveInvocationContext 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 getArgumentParameters(final ReflectiveInvocationContext invocationContext) { @@ -258,4 +323,7 @@ private boolean shouldHandle(final ExtensionContext extensionContext, .getOrComputeIfAbsent(key, ignored -> marker); return marker.equals(storedMarker); } + + private record ParameterCapture(List parameters) { + } } diff --git a/allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiterLegacyParameterInfoSupport.java b/allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiterLegacyParameterInfoSupport.java new file mode 100644 index 000000000..261ecef53 --- /dev/null +++ b/allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiterLegacyParameterInfoSupport.java @@ -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 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 getClassParameters(final ExtensionContext extensionContext, + final Method testMethod) { + return collectParameters( + extensionContext, + info -> !testMethod.equals(info.getDeclarations().getSourceElement()) + ); + } + + private static List collectParameters(final ExtensionContext extensionContext, + final Predicate filter) { + final List result = new ArrayList<>(); + final Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); + Optional 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 toParameters(final ParameterInfo info) { + final ArgumentsAccessor arguments = info.getArguments(); + final List 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; + } +} diff --git a/allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiterParameterInfoSupport.java b/allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiterParameterInfoSupport.java index 7e8ca5b1c..a7507dd56 100644 --- a/allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiterParameterInfoSupport.java +++ b/allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiterParameterInfoSupport.java @@ -30,11 +30,12 @@ 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 { @@ -42,6 +43,16 @@ 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 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. @@ -52,6 +63,14 @@ private AllureJupiterParameterInfoSupport() { */ static List getClassParameters(final ExtensionContext extensionContext, final Method testMethod) { + return collectParameters( + extensionContext, + info -> !testMethod.equals(info.getDeclarations().getSourceElement()) + ); + } + + private static List collectParameters(final ExtensionContext extensionContext, + final Predicate filter) { final List result = new ArrayList<>(); final Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); Optional current = Optional.of(extensionContext); @@ -60,7 +79,7 @@ static List 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)); } diff --git a/allure-jupiter/src/test/java/io/qameta/allure/jupiter/AllureJupiterTest.java b/allure-jupiter/src/test/java/io/qameta/allure/jupiter/AllureJupiterTest.java index 3fb1916de..60e9d627f 100644 --- a/allure-jupiter/src/test/java/io/qameta/allure/jupiter/AllureJupiterTest.java +++ b/allure-jupiter/src/test/java/io/qameta/allure/jupiter/AllureJupiterTest.java @@ -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; @@ -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); diff --git a/allure-jupiter/src/test/java/io/qameta/allure/jupiter/features/ParameterizedBeforeEachTests.java b/allure-jupiter/src/test/java/io/qameta/allure/jupiter/features/ParameterizedBeforeEachTests.java new file mode 100644 index 000000000..ce165deb6 --- /dev/null +++ b/allure-jupiter/src/test/java/io/qameta/allure/jupiter/features/ParameterizedBeforeEachTests.java @@ -0,0 +1,46 @@ +/* + * 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.features; + +import io.qameta.allure.Param; +import io.qameta.allure.jupiter.AllureJupiter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +@ExtendWith(AllureJupiter.class) +public class ParameterizedBeforeEachTests { + + private static boolean failBeforeEach; + + public static void failBeforeEach(final boolean fail) { + failBeforeEach = fail; + } + + @BeforeEach + void setUp() { + if (failBeforeEach) { + throw new IllegalStateException("fail in beforeEach"); + } + } + + @ParameterizedTest + @CsvSource("first value, second value") + void parameterizedTest(@Param("first") final String first, + @Param("second") final String second) { + } +}