From bb7e6fec4fa8e4dc8fef97df44f81ad1a30ba343 Mon Sep 17 00:00:00 2001 From: qameta-ci Date: Thu, 9 Apr 2026 09:25:34 +0000 Subject: [PATCH 01/13] set next development version 2.35 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index b41179d6..b5be834c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=2.34.0 +version=2.35-SNAPSHOT org.gradle.daemon=true org.gradle.parallel=true From bee34be982bdac7ff97e8350bb9ed58742a41610 Mon Sep 17 00:00:00 2001 From: Dmitry Baev Date: Thu, 9 Apr 2026 11:00:59 +0100 Subject: [PATCH 02/13] Rename allure-junit5 to allure-jupiter (via #1262) --- .github/ISSUE_TEMPLATE/bug_report.yml | 3 + .github/labeler.yml | 4 +- README.md | 11 +- allure-bom/build.gradle.kts | 2 + .../org.junit.jupiter.api.extension.Extension | 1 - .../org.junit.jupiter.api.extension.Extension | 1 - .../README.MD | 4 +- .../build.gradle.kts | 13 +- .../junit5assert/AllureJunit5Assert.java | 26 +++ .../jupiterassert/AllureJupiterAssert.java | 28 ++- .../src/main/resources/META-INF/aop-ajc.xml | 2 +- .../junit5assert/AllureJunit5AssertTest.java | 0 .../AllureJupiterAssertTest.java | 41 +++++ .../src/test/resources/allure.properties | 0 .../build.gradle.kts | 9 +- .../io/qameta/allure/junit5/AllureJunit5.java | 26 +++ .../qameta/allure/jupiter/AllureJupiter.java | 31 +++- .../org.junit.jupiter.api.extension.Extension | 1 + .../AllureJunit5Junit6CompatibilityTest.java | 27 ++- .../allure/junit5/AllureJunit5Test.java | 0 .../AfterEachFixtureBrokenSupport.java | 0 .../junit5/features/AllFixtureSupport.java | 0 .../BeforeAllFixtureFailureSupport.java | 0 .../BeforeEachFixtureBrokenSupport.java | 0 .../junit5/features/EachFixtureSupport.java | 0 ...ParameterisedBlankParameterValueTests.java | 0 .../ParameterisedPrimitivesTests.java | 0 .../junit5/features/ParameterisedTests.java | 0 .../ParameterisedWithInjectablesTests.java | 0 .../junit5/features/SkipOtherInjectables.java | 0 .../AllureJupiterJunit6CompatibilityTest.java | 165 ++++++++++++++++++ .../org.junit.jupiter.api.extension.Extension | 1 + .../src/test/resources/allure.properties | 0 allure-scalatest/build.gradle.kts | 7 - build.gradle.kts | 87 +++++++-- settings.gradle.kts | 4 +- 36 files changed, 426 insertions(+), 68 deletions(-) delete mode 100644 allure-junit5/src/main/services/org.junit.jupiter.api.extension.Extension delete mode 100644 allure-junit5/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension rename {allure-junit5-assert => allure-jupiter-assert}/README.MD (78%) rename {allure-junit5-assert => allure-jupiter-assert}/build.gradle.kts (64%) create mode 100644 allure-jupiter-assert/src/main/java/io/qameta/allure/junit5assert/AllureJunit5Assert.java rename allure-junit5-assert/src/main/java/io/qameta/allure/junit5assert/AllureJunit5Assert.java => allure-jupiter-assert/src/main/java/io/qameta/allure/jupiterassert/AllureJupiterAssert.java (87%) rename {allure-junit5-assert => allure-jupiter-assert}/src/main/resources/META-INF/aop-ajc.xml (56%) rename {allure-junit5-assert => allure-jupiter-assert}/src/test/java/io/qameta/allure/junit5assert/AllureJunit5AssertTest.java (100%) create mode 100644 allure-jupiter-assert/src/test/java/io/qameta/allure/jupiterassert/AllureJupiterAssertTest.java rename {allure-junit5-assert => allure-jupiter-assert}/src/test/resources/allure.properties (100%) rename {allure-junit5 => allure-jupiter}/build.gradle.kts (84%) create mode 100644 allure-jupiter/src/main/java/io/qameta/allure/junit5/AllureJunit5.java rename allure-junit5/src/main/java/io/qameta/allure/junit5/AllureJunit5.java => allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiter.java (88%) create mode 100644 allure-jupiter/src/main/services/org.junit.jupiter.api.extension.Extension rename {allure-junit5 => allure-jupiter}/src/test/java/io/qameta/allure/junit5/AllureJunit5Junit6CompatibilityTest.java (87%) rename {allure-junit5 => allure-jupiter}/src/test/java/io/qameta/allure/junit5/AllureJunit5Test.java (100%) rename {allure-junit5 => allure-jupiter}/src/test/java/io/qameta/allure/junit5/features/AfterEachFixtureBrokenSupport.java (100%) rename {allure-junit5 => allure-jupiter}/src/test/java/io/qameta/allure/junit5/features/AllFixtureSupport.java (100%) rename {allure-junit5 => allure-jupiter}/src/test/java/io/qameta/allure/junit5/features/BeforeAllFixtureFailureSupport.java (100%) rename {allure-junit5 => allure-jupiter}/src/test/java/io/qameta/allure/junit5/features/BeforeEachFixtureBrokenSupport.java (100%) rename {allure-junit5 => allure-jupiter}/src/test/java/io/qameta/allure/junit5/features/EachFixtureSupport.java (100%) rename {allure-junit5 => allure-jupiter}/src/test/java/io/qameta/allure/junit5/features/ParameterisedBlankParameterValueTests.java (100%) rename {allure-junit5 => allure-jupiter}/src/test/java/io/qameta/allure/junit5/features/ParameterisedPrimitivesTests.java (100%) rename {allure-junit5 => allure-jupiter}/src/test/java/io/qameta/allure/junit5/features/ParameterisedTests.java (100%) rename {allure-junit5 => allure-jupiter}/src/test/java/io/qameta/allure/junit5/features/ParameterisedWithInjectablesTests.java (100%) rename {allure-junit5 => allure-jupiter}/src/test/java/io/qameta/allure/junit5/features/SkipOtherInjectables.java (100%) create mode 100644 allure-jupiter/src/test/java/io/qameta/allure/jupiter/AllureJupiterJunit6CompatibilityTest.java create mode 100644 allure-jupiter/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension rename {allure-junit5 => allure-jupiter}/src/test/resources/allure.properties (100%) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index bb09719d..319290ea 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -45,7 +45,10 @@ body: - allure-jsonunit - allure-junit-platform - allure-junit4 + - allure-jupiter + - allure-jupiter-assert - allure-junit5 + - allure-junit5-assert - allure-karate - allure-okhttp - allure-okhttp3 diff --git a/.github/labeler.yml b/.github/labeler.yml index a419c97f..367271a0 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -48,8 +48,8 @@ - "allure-junit4-aspect/**" "theme:junit-platform": - - "allure-junit5/**" - - "allure-junit5-assert/**" + - "allure-jupiter/**" + - "allure-jupiter-assert/**" - "allure-junit-platform/**" "theme:karate": diff --git a/README.md b/README.md index e452787b..5180b507 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,12 @@ - 📚 Example project — https://github.com/allure-examples?q=topic%3Ajunit4 - ✅ Generate a project in 10 seconds via Allure Start - https://allurereport.org/start/ - -## JUnit 5 - -- 🚀 Documentation — https://allurereport.org/docs/junit5/ -- 📚 Example project — https://github.com/allure-examples?q=topic%3Ajunit5 -- ✅ Generate a project in 10 seconds via Allure Start - https://allurereport.org/start/ +## JUnit Jupiter (JUnit 5 and 6) + +- 🚀 Documentation — https://allurereport.org/docs/junit5/ +- 📚 Example project — https://github.com/allure-examples?q=topic%3Ajunit5 +- ✅ Generate a project in 10 seconds via Allure Start - https://allurereport.org/start/ +- 🧩 Use `io.qameta.allure:allure-jupiter` for new setups. `allure-junit5` remains available as a deprecated compatibility alias during migration. ## Cucumber JVM diff --git a/allure-bom/build.gradle.kts b/allure-bom/build.gradle.kts index 9593300a..0d83d95e 100644 --- a/allure-bom/build.gradle.kts +++ b/allure-bom/build.gradle.kts @@ -8,6 +8,8 @@ dependencies { constraints { rootProject.subprojects.sorted() .forEach { api("${it.group}:${it.name}:${it.version}") } + api("io.qameta.allure:allure-junit5:${project.version}") + api("io.qameta.allure:allure-junit5-assert:${project.version}") } } diff --git a/allure-junit5/src/main/services/org.junit.jupiter.api.extension.Extension b/allure-junit5/src/main/services/org.junit.jupiter.api.extension.Extension deleted file mode 100644 index 4a1ec18c..00000000 --- a/allure-junit5/src/main/services/org.junit.jupiter.api.extension.Extension +++ /dev/null @@ -1 +0,0 @@ -io.qameta.allure.junit5.AllureJunit5 diff --git a/allure-junit5/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/allure-junit5/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension deleted file mode 100644 index 4a1ec18c..00000000 --- a/allure-junit5/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension +++ /dev/null @@ -1 +0,0 @@ -io.qameta.allure.junit5.AllureJunit5 diff --git a/allure-junit5-assert/README.MD b/allure-jupiter-assert/README.MD similarity index 78% rename from allure-junit5-assert/README.MD rename to allure-jupiter-assert/README.MD index 5124d63c..1705bcae 100644 --- a/allure-junit5-assert/README.MD +++ b/allure-jupiter-assert/README.MD @@ -1,4 +1,6 @@ -Adding JUnit5-assert may lead to java.lang.OutOfMemoryError: Java heap space +Adding Jupiter assert may lead to java.lang.OutOfMemoryError: Java heap space + +The primary artifact is now `allure-jupiter-assert`. `allure-junit5-assert` remains available as a deprecated compatibility alias during the transition. Having a huge class path may lead to OOM, because AspectJ processes all the classes. [Link to documentation](https://www.eclipse.org/aspectj/doc/released/devguide/ltw.html) diff --git a/allure-junit5-assert/build.gradle.kts b/allure-jupiter-assert/build.gradle.kts similarity index 64% rename from allure-junit5-assert/build.gradle.kts rename to allure-jupiter-assert/build.gradle.kts index dd541015..230fb503 100644 --- a/allure-junit5-assert/build.gradle.kts +++ b/allure-jupiter-assert/build.gradle.kts @@ -1,7 +1,7 @@ -description = "Allure Junit5 Assertions Integration" +description = "Allure Jupiter Assertions Integration" dependencies { - api(project(":allure-junit5")) + api(project(":allure-jupiter")) compileOnly("org.aspectj:aspectjrt") compileOnly("org.junit.jupiter:junit-jupiter-api") testAnnotationProcessor(project(":allure-descriptions-javadoc")) @@ -15,7 +15,7 @@ dependencies { tasks.jar { manifest { attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.junit5-assert" + "Automatic-Module-Name" to "io.qameta.allure.jupiterassert" )) } } @@ -24,3 +24,10 @@ tasks.test { useJUnitPlatform() } +publishing { + publications { + create("legacyJunit5Assert") { + artifactId = "allure-junit5-assert" + } + } +} diff --git a/allure-jupiter-assert/src/main/java/io/qameta/allure/junit5assert/AllureJunit5Assert.java b/allure-jupiter-assert/src/main/java/io/qameta/allure/junit5assert/AllureJunit5Assert.java new file mode 100644 index 00000000..f7f04b0f --- /dev/null +++ b/allure-jupiter-assert/src/main/java/io/qameta/allure/junit5assert/AllureJunit5Assert.java @@ -0,0 +1,26 @@ +/* + * 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.junit5assert; + +import io.qameta.allure.jupiterassert.AllureJupiterAssert; + +/** + * @author legionivo (Andrey Konovka). + * @deprecated use {@link AllureJupiterAssert}. + */ +@Deprecated +public class AllureJunit5Assert extends AllureJupiterAssert { +} diff --git a/allure-junit5-assert/src/main/java/io/qameta/allure/junit5assert/AllureJunit5Assert.java b/allure-jupiter-assert/src/main/java/io/qameta/allure/jupiterassert/AllureJupiterAssert.java similarity index 87% rename from allure-junit5-assert/src/main/java/io/qameta/allure/junit5assert/AllureJunit5Assert.java rename to allure-jupiter-assert/src/main/java/io/qameta/allure/jupiterassert/AllureJupiterAssert.java index e7fad008..a259fdbb 100644 --- a/allure-junit5-assert/src/main/java/io/qameta/allure/junit5assert/AllureJunit5Assert.java +++ b/allure-jupiter-assert/src/main/java/io/qameta/allure/jupiterassert/AllureJupiterAssert.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.qameta.allure.junit5assert; +package io.qameta.allure.jupiterassert; import io.qameta.allure.Allure; import io.qameta.allure.AllureLifecycle; @@ -41,13 +41,12 @@ */ @SuppressWarnings("all") @Aspect -public class AllureJunit5Assert { +public class AllureJupiterAssert { - private static final Logger LOGGER = LoggerFactory.getLogger(AllureJunit5Assert.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AllureJupiterAssert.class); private StepResult stepResult; - - private static InheritableThreadLocal lifecycle = new InheritableThreadLocal() { + private static final InheritableThreadLocal LIFECYCLE = new InheritableThreadLocal() { @Override protected AllureLifecycle initialValue() { return Allure.getLifecycle(); @@ -69,11 +68,10 @@ public void stepStart(final JoinPoint joinPoint) { if (joinPoint.getArgs().length > 1) { final String uuid = UUID.randomUUID().toString(); final String assertName = joinPoint.getSignature().getName(); - String name; + final String name; if (joinPoint.getSignature().getName().equalsIgnoreCase("assertAll")) { - name = String.format("assert All in " + " \'%s\'", joinPoint.getArgs()[0].toString()); + name = String.format("assert All in " + " '%s'", joinPoint.getArgs()[0].toString()); } else { - final String actual = joinPoint.getArgs().length > 0 ? ObjectUtils.toString(joinPoint.getArgs()[1]) : ""; @@ -83,12 +81,13 @@ public void stepStart(final JoinPoint joinPoint) { final List assertArray = Arrays.asList(assertName.split("(?=[A-Z])")); if (assertArray.size() >= 3) { - name = String.format(assertArray.get(0) + " " + assertArray.get(1) + " \'%s\'", expected) + name = String.format(assertArray.get(0) + " " + assertArray.get(1) + " '%s'", expected) + " " + String.format(assertArray.stream() .skip(2) - .collect(Collectors.joining(" ")) + " \'%s\'", actual); + .collect(Collectors.joining(" ")) + " '%s'", actual); } else { - name = String.format(assertArray.get(0) + " \'%s\'", expected) + " " + String.format(assertArray.get(1) + " \'%s\'", actual); + name = String.format(assertArray.get(0) + " '%s'", expected) + + " " + String.format(assertArray.get(1) + " '%s'", actual); } } final StepResult result = new StepResult() @@ -101,7 +100,7 @@ public void stepStart(final JoinPoint joinPoint) { : ""; final String uuid = UUID.randomUUID().toString(); final String assertName = joinPoint.getSignature().getName(); - final String name = String.format(assertName + " \'%s\'", actual); + final String name = String.format(assertName + " '%s'", actual); final StepResult result = new StepResult() .setName(name) @@ -129,11 +128,10 @@ public void stepStop() { * @param allure allure lifecycle to set. */ public static void setLifecycle(final AllureLifecycle allure) { - lifecycle.set(allure); + LIFECYCLE.set(allure); } public static AllureLifecycle getLifecycle() { - return lifecycle.get(); + return LIFECYCLE.get(); } - } diff --git a/allure-junit5-assert/src/main/resources/META-INF/aop-ajc.xml b/allure-jupiter-assert/src/main/resources/META-INF/aop-ajc.xml similarity index 56% rename from allure-junit5-assert/src/main/resources/META-INF/aop-ajc.xml rename to allure-jupiter-assert/src/main/resources/META-INF/aop-ajc.xml index 1ce84d6d..f4a03961 100644 --- a/allure-junit5-assert/src/main/resources/META-INF/aop-ajc.xml +++ b/allure-jupiter-assert/src/main/resources/META-INF/aop-ajc.xml @@ -1,6 +1,6 @@ - + diff --git a/allure-junit5-assert/src/test/java/io/qameta/allure/junit5assert/AllureJunit5AssertTest.java b/allure-jupiter-assert/src/test/java/io/qameta/allure/junit5assert/AllureJunit5AssertTest.java similarity index 100% rename from allure-junit5-assert/src/test/java/io/qameta/allure/junit5assert/AllureJunit5AssertTest.java rename to allure-jupiter-assert/src/test/java/io/qameta/allure/junit5assert/AllureJunit5AssertTest.java diff --git a/allure-jupiter-assert/src/test/java/io/qameta/allure/jupiterassert/AllureJupiterAssertTest.java b/allure-jupiter-assert/src/test/java/io/qameta/allure/jupiterassert/AllureJupiterAssertTest.java new file mode 100644 index 00000000..1da892a9 --- /dev/null +++ b/allure-jupiter-assert/src/test/java/io/qameta/allure/jupiterassert/AllureJupiterAssertTest.java @@ -0,0 +1,41 @@ +/* + * 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.jupiterassert; + +import io.qameta.allure.model.StepResult; +import io.qameta.allure.model.TestResult; +import io.qameta.allure.test.AllureResults; +import org.junit.jupiter.api.Test; + +import static io.qameta.allure.test.RunUtils.runWithinTestContext; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class AllureJupiterAssertTest { + + @Test + void shouldHandleAssertEquals() { + final AllureResults results = runWithinTestContext( + () -> assertEquals("expectedString", "actualString"), + AllureJupiterAssert::setLifecycle + ); + + assertThat(results.getTestResults()) + .flatExtracting(TestResult::getSteps) + .extracting(StepResult::getName) + .containsExactly("assert 'expectedString' Equals 'actualString'"); + } +} diff --git a/allure-junit5-assert/src/test/resources/allure.properties b/allure-jupiter-assert/src/test/resources/allure.properties similarity index 100% rename from allure-junit5-assert/src/test/resources/allure.properties rename to allure-jupiter-assert/src/test/resources/allure.properties diff --git a/allure-junit5/build.gradle.kts b/allure-jupiter/build.gradle.kts similarity index 84% rename from allure-junit5/build.gradle.kts rename to allure-jupiter/build.gradle.kts index 37288580..dd603387 100644 --- a/allure-junit5/build.gradle.kts +++ b/allure-jupiter/build.gradle.kts @@ -1,4 +1,4 @@ -description = "Allure JUnit 5 Integration" +description = "Allure Jupiter Integration" dependencies { api(project(":allure-junit-platform")) @@ -19,7 +19,7 @@ dependencies { tasks.jar { manifest { attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.junit5" + "Automatic-Module-Name" to "io.qameta.allure.jupiter" )) } from("src/main/services") { @@ -43,6 +43,9 @@ publishing { named("maven") { artifact(spiOffJar) } + create("legacyJunit5") { + artifactId = "allure-junit5" + artifact(spiOffJar) + } } } - diff --git a/allure-jupiter/src/main/java/io/qameta/allure/junit5/AllureJunit5.java b/allure-jupiter/src/main/java/io/qameta/allure/junit5/AllureJunit5.java new file mode 100644 index 00000000..2231d34e --- /dev/null +++ b/allure-jupiter/src/main/java/io/qameta/allure/junit5/AllureJunit5.java @@ -0,0 +1,26 @@ +/* + * 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.junit5; + +import io.qameta.allure.jupiter.AllureJupiter; + +/** + * @author charlie (Dmitry Baev). + * @deprecated use {@link AllureJupiter}. + */ +@Deprecated +public class AllureJunit5 extends AllureJupiter { +} diff --git a/allure-junit5/src/main/java/io/qameta/allure/junit5/AllureJunit5.java b/allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiter.java similarity index 88% rename from allure-junit5/src/main/java/io/qameta/allure/junit5/AllureJunit5.java rename to allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiter.java index 3d1019a2..1641ed1f 100644 --- a/allure-junit5/src/main/java/io/qameta/allure/junit5/AllureJunit5.java +++ b/allure-jupiter/src/main/java/io/qameta/allure/jupiter/AllureJupiter.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.qameta.allure.junit5; +package io.qameta.allure.jupiter; import io.qameta.allure.Param; import io.qameta.allure.model.Status; @@ -27,8 +27,8 @@ import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.HashMap; -import java.util.Map; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.UUID; @@ -50,12 +50,18 @@ * @author charlie (Dmitry Baev). */ @SuppressWarnings("MultipleStringLiterals") -public class AllureJunit5 implements InvocationInterceptor { +public class AllureJupiter implements InvocationInterceptor { + + private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create(AllureJupiter.class); @Override public void interceptTestTemplateMethod(final Invocation invocation, final ReflectiveInvocationContext invocationContext, final ExtensionContext extensionContext) throws Throwable { + if (!shouldHandle(extensionContext, "template", invocationContext.getExecutable())) { + invocation.proceed(); + return; + } sendParameterEvent(invocationContext, extensionContext); invocation.proceed(); } @@ -134,6 +140,10 @@ protected void processFixture(final String type, final Invocation invocation, final ReflectiveInvocationContext invocationContext, final ExtensionContext extensionContext) throws Throwable { + if (!shouldHandle(extensionContext, type, invocationContext.getExecutable())) { + invocation.proceed(); + return; + } final String uuid = UUID.randomUUID().toString(); try { extensionContext.publishReportEntry(wrap(buildStartEvent( @@ -206,4 +216,19 @@ public Map wrap(final Map data) { ); return res; } + + private boolean shouldHandle(final ExtensionContext extensionContext, + final String eventType, + final Method method) { + final Object marker = new Object(); + final String key = String.join(":", + extensionContext.getUniqueId(), + eventType, + method.toGenericString() + ); + final Object storedMarker = extensionContext.getRoot() + .getStore(NAMESPACE) + .getOrComputeIfAbsent(key, ignored -> marker); + return marker.equals(storedMarker); + } } diff --git a/allure-jupiter/src/main/services/org.junit.jupiter.api.extension.Extension b/allure-jupiter/src/main/services/org.junit.jupiter.api.extension.Extension new file mode 100644 index 00000000..17a07514 --- /dev/null +++ b/allure-jupiter/src/main/services/org.junit.jupiter.api.extension.Extension @@ -0,0 +1 @@ +io.qameta.allure.jupiter.AllureJupiter diff --git a/allure-junit5/src/test/java/io/qameta/allure/junit5/AllureJunit5Junit6CompatibilityTest.java b/allure-jupiter/src/test/java/io/qameta/allure/junit5/AllureJunit5Junit6CompatibilityTest.java similarity index 87% rename from allure-junit5/src/test/java/io/qameta/allure/junit5/AllureJunit5Junit6CompatibilityTest.java rename to allure-jupiter/src/test/java/io/qameta/allure/junit5/AllureJunit5Junit6CompatibilityTest.java index a9d85cc2..c1d5171c 100644 --- a/allure-junit5/src/test/java/io/qameta/allure/junit5/AllureJunit5Junit6CompatibilityTest.java +++ b/allure-jupiter/src/test/java/io/qameta/allure/junit5/AllureJunit5Junit6CompatibilityTest.java @@ -90,12 +90,17 @@ void shouldCaptureParametersWithParamAnnotationOnJunit6() { .toList(); assertThat(allParams) - .isNotEmpty() + .filteredOn(parameter -> "id".equals(parameter.getName())) .extracting(Parameter::getName, Parameter::getValue) - .contains( + .containsExactlyInAnyOrder( tuple("id", "a"), tuple("id", "b") ); + + assertThat(allParams) + .filteredOn(parameter -> "id".equals(parameter.getName())) + .extracting(Parameter::getName, Parameter::getValue) + .doesNotHaveDuplicates(); } @Test @@ -111,30 +116,34 @@ void shouldCaptureFixturesAndStepsOnJunit6() { assertThat(results.getTestResultContainers()) .flatExtracting(TestResultContainer::getBefores) - .extracting(FixtureResult::getStatus) - .contains(Status.PASSED); + .extracting(FixtureResult::getName, FixtureResult::getStatus) + .containsExactly( + tuple("setUp", Status.PASSED) + ); assertThat(results.getTestResultContainers()) .flatExtracting(TestResultContainer::getAfters) - .extracting(FixtureResult::getStatus) - .contains(Status.PASSED); + .extracting(FixtureResult::getName, FixtureResult::getStatus) + .containsExactly( + tuple("tearDown", Status.PASSED) + ); assertThat(results.getTestResultContainers()) .flatExtracting(TestResultContainer::getBefores) .flatExtracting(FixtureResult::getSteps) .extracting(StepResult::getName) - .contains("before step"); + .containsExactly("before step"); assertThat(results.getTestResults()) .flatExtracting(TestResult::getSteps) .extracting(StepResult::getName) - .contains("test step"); + .containsExactly("test step"); assertThat(results.getTestResultContainers()) .flatExtracting(TestResultContainer::getAfters) .flatExtracting(FixtureResult::getSteps) .extracting(StepResult::getName) - .contains("after step"); + .containsExactly("after step"); } @io.qameta.allure.Step("Run classes {classes}") diff --git a/allure-junit5/src/test/java/io/qameta/allure/junit5/AllureJunit5Test.java b/allure-jupiter/src/test/java/io/qameta/allure/junit5/AllureJunit5Test.java similarity index 100% rename from allure-junit5/src/test/java/io/qameta/allure/junit5/AllureJunit5Test.java rename to allure-jupiter/src/test/java/io/qameta/allure/junit5/AllureJunit5Test.java diff --git a/allure-junit5/src/test/java/io/qameta/allure/junit5/features/AfterEachFixtureBrokenSupport.java b/allure-jupiter/src/test/java/io/qameta/allure/junit5/features/AfterEachFixtureBrokenSupport.java similarity index 100% rename from allure-junit5/src/test/java/io/qameta/allure/junit5/features/AfterEachFixtureBrokenSupport.java rename to allure-jupiter/src/test/java/io/qameta/allure/junit5/features/AfterEachFixtureBrokenSupport.java diff --git a/allure-junit5/src/test/java/io/qameta/allure/junit5/features/AllFixtureSupport.java b/allure-jupiter/src/test/java/io/qameta/allure/junit5/features/AllFixtureSupport.java similarity index 100% rename from allure-junit5/src/test/java/io/qameta/allure/junit5/features/AllFixtureSupport.java rename to allure-jupiter/src/test/java/io/qameta/allure/junit5/features/AllFixtureSupport.java diff --git a/allure-junit5/src/test/java/io/qameta/allure/junit5/features/BeforeAllFixtureFailureSupport.java b/allure-jupiter/src/test/java/io/qameta/allure/junit5/features/BeforeAllFixtureFailureSupport.java similarity index 100% rename from allure-junit5/src/test/java/io/qameta/allure/junit5/features/BeforeAllFixtureFailureSupport.java rename to allure-jupiter/src/test/java/io/qameta/allure/junit5/features/BeforeAllFixtureFailureSupport.java diff --git a/allure-junit5/src/test/java/io/qameta/allure/junit5/features/BeforeEachFixtureBrokenSupport.java b/allure-jupiter/src/test/java/io/qameta/allure/junit5/features/BeforeEachFixtureBrokenSupport.java similarity index 100% rename from allure-junit5/src/test/java/io/qameta/allure/junit5/features/BeforeEachFixtureBrokenSupport.java rename to allure-jupiter/src/test/java/io/qameta/allure/junit5/features/BeforeEachFixtureBrokenSupport.java diff --git a/allure-junit5/src/test/java/io/qameta/allure/junit5/features/EachFixtureSupport.java b/allure-jupiter/src/test/java/io/qameta/allure/junit5/features/EachFixtureSupport.java similarity index 100% rename from allure-junit5/src/test/java/io/qameta/allure/junit5/features/EachFixtureSupport.java rename to allure-jupiter/src/test/java/io/qameta/allure/junit5/features/EachFixtureSupport.java diff --git a/allure-junit5/src/test/java/io/qameta/allure/junit5/features/ParameterisedBlankParameterValueTests.java b/allure-jupiter/src/test/java/io/qameta/allure/junit5/features/ParameterisedBlankParameterValueTests.java similarity index 100% rename from allure-junit5/src/test/java/io/qameta/allure/junit5/features/ParameterisedBlankParameterValueTests.java rename to allure-jupiter/src/test/java/io/qameta/allure/junit5/features/ParameterisedBlankParameterValueTests.java diff --git a/allure-junit5/src/test/java/io/qameta/allure/junit5/features/ParameterisedPrimitivesTests.java b/allure-jupiter/src/test/java/io/qameta/allure/junit5/features/ParameterisedPrimitivesTests.java similarity index 100% rename from allure-junit5/src/test/java/io/qameta/allure/junit5/features/ParameterisedPrimitivesTests.java rename to allure-jupiter/src/test/java/io/qameta/allure/junit5/features/ParameterisedPrimitivesTests.java diff --git a/allure-junit5/src/test/java/io/qameta/allure/junit5/features/ParameterisedTests.java b/allure-jupiter/src/test/java/io/qameta/allure/junit5/features/ParameterisedTests.java similarity index 100% rename from allure-junit5/src/test/java/io/qameta/allure/junit5/features/ParameterisedTests.java rename to allure-jupiter/src/test/java/io/qameta/allure/junit5/features/ParameterisedTests.java diff --git a/allure-junit5/src/test/java/io/qameta/allure/junit5/features/ParameterisedWithInjectablesTests.java b/allure-jupiter/src/test/java/io/qameta/allure/junit5/features/ParameterisedWithInjectablesTests.java similarity index 100% rename from allure-junit5/src/test/java/io/qameta/allure/junit5/features/ParameterisedWithInjectablesTests.java rename to allure-jupiter/src/test/java/io/qameta/allure/junit5/features/ParameterisedWithInjectablesTests.java diff --git a/allure-junit5/src/test/java/io/qameta/allure/junit5/features/SkipOtherInjectables.java b/allure-jupiter/src/test/java/io/qameta/allure/junit5/features/SkipOtherInjectables.java similarity index 100% rename from allure-junit5/src/test/java/io/qameta/allure/junit5/features/SkipOtherInjectables.java rename to allure-jupiter/src/test/java/io/qameta/allure/junit5/features/SkipOtherInjectables.java diff --git a/allure-jupiter/src/test/java/io/qameta/allure/jupiter/AllureJupiterJunit6CompatibilityTest.java b/allure-jupiter/src/test/java/io/qameta/allure/jupiter/AllureJupiterJunit6CompatibilityTest.java new file mode 100644 index 00000000..f40fe2bd --- /dev/null +++ b/allure-jupiter/src/test/java/io/qameta/allure/jupiter/AllureJupiterJunit6CompatibilityTest.java @@ -0,0 +1,165 @@ +/* + * 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.Allure; +import io.qameta.allure.Param; +import io.qameta.allure.junitplatform.AllureJunitPlatform; +import io.qameta.allure.model.FixtureResult; +import io.qameta.allure.model.Parameter; +import io.qameta.allure.model.Status; +import io.qameta.allure.model.StepResult; +import io.qameta.allure.model.TestResult; +import io.qameta.allure.model.TestResultContainer; +import io.qameta.allure.test.AllureResults; +import io.qameta.allure.test.RunUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.platform.engine.discovery.ClassSelector; +import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.launcher.Launcher; +import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.core.LauncherConfig; +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; +import org.junit.platform.launcher.core.LauncherFactory; + +import java.util.List; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; + +@Tag("junit6-compat") +@SuppressWarnings("unused") +class AllureJupiterJunit6CompatibilityTest { + + @ExtendWith(AllureJupiter.class) + static class CompatParametersTest { + + @org.junit.jupiter.params.ParameterizedTest + @org.junit.jupiter.params.provider.ValueSource(strings = {"a", "b"}) + void paramTest(@Param("id") final String value) { + } + } + + @Nested + @ExtendWith(AllureJupiter.class) + class CompatFixtures { + + @BeforeEach + void setUp() { + Allure.step("before step"); + } + + @Test + void testBody() { + Allure.step("test step"); + } + + @AfterEach + void tearDown() { + Allure.step("after step"); + } + } + + @Test + void shouldCaptureParametersWithParamAnnotationOnJunit6() { + final AllureResults results = runWithLauncher(CompatParametersTest.class); + + assertThat(results.getTestResults()).isNotEmpty(); + + final List allParams = results.getTestResults().stream() + .flatMap(tr -> tr.getParameters().stream()) + .toList(); + + assertThat(allParams) + .filteredOn(parameter -> "id".equals(parameter.getName())) + .extracting(Parameter::getName, Parameter::getValue) + .containsExactlyInAnyOrder( + tuple("id", "a"), + tuple("id", "b") + ); + } + + @Test + void shouldCaptureFixturesAndStepsOnJunit6() { + final AllureResults results = runWithLauncher(CompatFixtures.class); + + assertThat(results.getTestResults()).hasSize(1); + final TestResult testResult = results.getTestResults().get(0); + + assertThat(results.getTestResultContainers()) + .flatExtracting(TestResultContainer::getChildren) + .contains(testResult.getUuid()); + + assertThat(results.getTestResultContainers()) + .flatExtracting(TestResultContainer::getBefores) + .extracting(FixtureResult::getName, FixtureResult::getStatus) + .containsExactly( + tuple("setUp", Status.PASSED) + ); + + assertThat(results.getTestResultContainers()) + .flatExtracting(TestResultContainer::getAfters) + .extracting(FixtureResult::getName, FixtureResult::getStatus) + .containsExactly( + tuple("tearDown", Status.PASSED) + ); + + assertThat(results.getTestResultContainers()) + .flatExtracting(TestResultContainer::getBefores) + .flatExtracting(FixtureResult::getSteps) + .extracting(StepResult::getName) + .containsExactly("before step"); + + assertThat(results.getTestResults()) + .flatExtracting(TestResult::getSteps) + .extracting(StepResult::getName) + .containsExactly("test step"); + + assertThat(results.getTestResultContainers()) + .flatExtracting(TestResultContainer::getAfters) + .flatExtracting(FixtureResult::getSteps) + .extracting(StepResult::getName) + .containsExactly("after step"); + } + + @io.qameta.allure.Step("Run classes {classes}") + private AllureResults runWithLauncher(final Class... classes) { + return RunUtils.runTests(lifecycle -> { + final ClassSelector[] selectors = Stream.of(classes) + .map(DiscoverySelectors::selectClass) + .toArray(ClassSelector[]::new); + + final LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request() + .configurationParameter("junit.jupiter.extensions.autodetection.enabled", "true") + .selectors(selectors) + .build(); + + final LauncherConfig config = LauncherConfig.builder() + .enableTestExecutionListenerAutoRegistration(false) + .addTestExecutionListeners(new AllureJunitPlatform(lifecycle)) + .build(); + + final Launcher launcher = LauncherFactory.create(config); + launcher.execute(request); + }); + } +} diff --git a/allure-jupiter/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/allure-jupiter/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension new file mode 100644 index 00000000..17a07514 --- /dev/null +++ b/allure-jupiter/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension @@ -0,0 +1 @@ +io.qameta.allure.jupiter.AllureJupiter diff --git a/allure-junit5/src/test/resources/allure.properties b/allure-jupiter/src/test/resources/allure.properties similarity index 100% rename from allure-junit5/src/test/resources/allure.properties rename to allure-jupiter/src/test/resources/allure.properties diff --git a/allure-scalatest/build.gradle.kts b/allure-scalatest/build.gradle.kts index a5ecc550..f16998fe 100644 --- a/allure-scalatest/build.gradle.kts +++ b/allure-scalatest/build.gradle.kts @@ -74,13 +74,6 @@ publishing { } } -signing { - sign( - publishing.publications["crossBuildScala_212"], - publishing.publications["crossBuildScala_213"] - ) -} - dependencies { api(project(":allure-java-commons")) compileOnly("org.scalatest:scalatest_$scala213:3.2.19") diff --git a/build.gradle.kts b/build.gradle.kts index b3a82e14..ac086dec 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,6 +5,7 @@ val qualityConfigsDir by extra("$gradleScriptDir/quality-configs") val spotlessDtr by extra("$qualityConfigsDir/spotless") val libs = subprojects.filterNot { it.name in "allure-bom" } +val standardJavaLibs = libs.filterNot { it.name == "allure-scalatest" } tasks.withType(Wrapper::class) { gradleVersion = "8.5" @@ -60,6 +61,12 @@ configure(subprojects) { publishing { publications { withType().configureEach { + suppressAllPomMetadataWarnings() + versionMapping { + allVariants { + fromResolutionResult() + } + } pom { name.set(project.name) description.set("Module ${project.name} of Allure Framework.") @@ -103,19 +110,12 @@ configure(subprojects) { } } } - create("maven") { - suppressAllPomMetadataWarnings() - versionMapping { - allVariants { - fromResolutionResult() - } - } - } + create("maven") } } signing { - sign(publishing.publications["maven"]) + sign(publishing.publications) } tasks.withType().configureEach { @@ -375,17 +375,74 @@ configure(libs) { } } - publishing.publications.named("maven") { - pom { - from(components["java"]) - } - } - val allDepsInsight by tasks.creating(DependencyInsightReportTask::class) { showingAllVariants.set(true) } } +configure(standardJavaLibs) { + publishing.publications.withType().configureEach { + from(components["java"]) + } +} + +val verifyJupiterCompatibilityBridge by tasks.registering { + dependsOn( + ":allure-bom:generatePomFileForMavenPublication" + ) + + doLast { + fun publicationArtifactIds(projectPath: String): Set = + project(projectPath) + .extensions + .getByType(org.gradle.api.publish.PublishingExtension::class.java) + .publications + .withType(MavenPublication::class.java) + .mapTo(linkedSetOf()) { it.artifactId } + + val jupiterArtifactIds = publicationArtifactIds(":allure-jupiter") + check("allure-jupiter" in jupiterArtifactIds) { + "Expected :allure-jupiter to publish the primary allure-jupiter coordinate." + } + check("allure-junit5" in jupiterArtifactIds) { + "Expected :allure-jupiter to publish the legacy allure-junit5 alias." + } + + val jupiterAssertArtifactIds = publicationArtifactIds(":allure-jupiter-assert") + check("allure-jupiter-assert" in jupiterAssertArtifactIds) { + "Expected :allure-jupiter-assert to publish the primary allure-jupiter-assert coordinate." + } + check("allure-junit5-assert" in jupiterAssertArtifactIds) { + "Expected :allure-jupiter-assert to publish the legacy allure-junit5-assert alias." + } + + val bomPom = project(":allure-bom") + .layout + .buildDirectory + .file("publications/maven/pom-default.xml") + .get() + .asFile + .readText() + + check("allure-jupiter" in bomPom) { + "Expected allure-bom to manage allure-jupiter." + } + check("allure-junit5" in bomPom) { + "Expected allure-bom to manage the legacy allure-junit5 alias." + } + check("allure-jupiter-assert" in bomPom) { + "Expected allure-bom to manage allure-jupiter-assert." + } + check("allure-junit5-assert" in bomPom) { + "Expected allure-bom to manage the legacy allure-junit5-assert alias." + } + } +} + +tasks.check { + dependsOn(verifyJupiterCompatibilityBridge) +} + allure { version.set("2.19.0") } diff --git a/settings.gradle.kts b/settings.gradle.kts index bf49b841..ad6f5e51 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,8 +24,8 @@ include("allure-jsonunit") include("allure-junit-platform") include("allure-junit4") include("allure-junit4-aspect") -include("allure-junit5") -include("allure-junit5-assert") +include("allure-jupiter") +include("allure-jupiter-assert") include("allure-karate") include("allure-model") include("allure-okhttp") From d2c7b0ca5176d34316b7e53d9685a50c4d9eb8d8 Mon Sep 17 00:00:00 2001 From: Dmitry Baev Date: Thu, 9 Apr 2026 16:35:35 +0100 Subject: [PATCH 03/13] Modernize allure-spring-web for current Spring HTTP clients (via #1263) --- README.md | 58 +++++++-- allure-spring-web/build.gradle.kts | 4 +- .../allure/springweb/AllureRestTemplate.java | 7 +- .../springweb/AllureRestTemplateTest.java | 118 ++++++++++++++---- 4 files changed, 147 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 5180b507..f77b1f03 100644 --- a/README.md +++ b/README.md @@ -78,9 +78,9 @@ https://github.com/SeleniumHQ/selenium/wiki/Logging ``` -## Rest Assured - -Filter for rest-assured http client, that generates attachment for allure. +## Rest Assured + +Filter for rest-assured http client, that generates attachment for allure. ```xml @@ -96,14 +96,50 @@ Usage example: ``` You can specify custom templates, which should be placed in src/main/resources/tpl folder: ``` -.filter(new AllureRestAssured() - .withRequestTemplate("custom-http-request.ftl") - .withResponseTemplate("custom-http-response.ftl")) -``` - -## OkHttp - -Interceptor for OkHttp client, that generates attachment for allure. +.filter(new AllureRestAssured() + .withRequestTemplate("custom-http-request.ftl") + .withResponseTemplate("custom-http-response.ftl")) +``` + +## Spring Web + +Interceptor for Spring synchronous HTTP clients, that generates attachments for allure. + +```xml + + io.qameta.allure + allure-spring-web + $LATEST_VERSION + +``` + +Usage example with `RestClient`: +``` +RestClient restClient = RestClient.builder() + .requestFactory(new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory())) + .requestInterceptor(new AllureRestTemplate()) + .build(); +``` +Use a buffering request factory when the client should still be able to read the response body after Allure captures it. + +`RestTemplate` remains supported: +``` +RestTemplate restTemplate = new RestTemplate( + new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()) +); +restTemplate.setInterceptors(Collections.singletonList(new AllureRestTemplate())); +``` + +You can specify custom templates, which should be placed in src/main/resources/tpl folder: +``` +new AllureRestTemplate() + .setRequestTemplate("custom-http-request.ftl") + .setResponseTemplate("custom-http-response.ftl") +``` + +## OkHttp + +Interceptor for OkHttp client, that generates attachment for allure. ```xml diff --git a/allure-spring-web/build.gradle.kts b/allure-spring-web/build.gradle.kts index b7956681..ab11b8b2 100644 --- a/allure-spring-web/build.gradle.kts +++ b/allure-spring-web/build.gradle.kts @@ -1,6 +1,6 @@ description = "Allure Spring Web Integration" -val springWebVersion = "6.2.12" +val springWebVersion = "6.2.17" dependencies { api(project(":allure-attachments")) @@ -32,4 +32,4 @@ tasks.test { tasks.compileJava { options.release.set(17) -} \ No newline at end of file +} diff --git a/allure-spring-web/src/main/java/io/qameta/allure/springweb/AllureRestTemplate.java b/allure-spring-web/src/main/java/io/qameta/allure/springweb/AllureRestTemplate.java index fbf57c41..bfc5611f 100644 --- a/allure-spring-web/src/main/java/io/qameta/allure/springweb/AllureRestTemplate.java +++ b/allure-spring-web/src/main/java/io/qameta/allure/springweb/AllureRestTemplate.java @@ -36,7 +36,12 @@ import java.util.Map; /** - * Allure interceptor for spring rest template. + * Allure interceptor for Spring synchronous HTTP clients such as + * {@code RestTemplate} and {@code RestClient}. + *

+ * Since this interceptor reads the response body to create an attachment, + * configure a buffering request factory when the caller also needs to consume + * the response body after interception. */ public class AllureRestTemplate implements ClientHttpRequestInterceptor { diff --git a/allure-spring-web/src/test/java/io/qameta/allure/springweb/AllureRestTemplateTest.java b/allure-spring-web/src/test/java/io/qameta/allure/springweb/AllureRestTemplateTest.java index 3298e093..f8b21eaa 100644 --- a/allure-spring-web/src/test/java/io/qameta/allure/springweb/AllureRestTemplateTest.java +++ b/allure-spring-web/src/test/java/io/qameta/allure/springweb/AllureRestTemplateTest.java @@ -33,11 +33,13 @@ import org.springframework.http.ResponseEntity; import org.springframework.http.client.BufferingClientHttpRequestFactory; import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.web.client.RestClient; import org.springframework.web.client.RestTemplate; import java.util.Collection; import java.util.Collections; import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import static io.qameta.allure.test.RunUtils.runWithinTestContext; @@ -49,55 +51,119 @@ @SuppressWarnings("unchecked") public class AllureRestTemplateTest { - static Stream attachmentNameProvider() { - return Stream.of("Request", "Response"); + static Stream clientTypeProvider() { + return Stream.of(SpringClientType.values()); } @ParameterizedTest - @MethodSource(value = "attachmentNameProvider") - void shouldCreateAttachment(final String attachmentName) { - final AllureResults results = execute(); + @MethodSource("clientTypeProvider") + void shouldCreateAttachment(final SpringClientType clientType) { + final AllureResults results = execute(clientType).getAllureResults(); assertThat(results.getTestResults()) .flatExtracting(TestResult::getAttachments) .flatExtracting(Attachment::getName) - .contains(attachmentName); + .contains("Request", "Response"); } @ParameterizedTest - @MethodSource(value = "attachmentNameProvider") - void shouldCatchAttachmentBody(final String attachmentName) { - final AllureResults results = execute(); + @MethodSource("clientTypeProvider") + void shouldCatchAttachmentBody(final SpringClientType clientType) { + final AllureResults results = execute(clientType).getAllureResults(); - final Attachment found = results.getTestResults().stream() - .map(TestResult::getAttachments) - .flatMap(Collection::stream) - .filter(attachment -> Objects.equals(attachmentName, attachment.getName())) - .findAny() - .orElseThrow(() -> new RuntimeException("attachment not found")); - - assertThat(results.getAttachments()) - .containsKeys(found.getSource()); + Stream.of("Request", "Response") + .map(attachmentName -> findAttachment(results, attachmentName)) + .forEach(found -> assertThat(results.getAttachments()) + .containsKeys(found.getSource())); } - protected final AllureResults execute() { - final RestTemplate restTemplate = new RestTemplate(new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory())); - restTemplate.setInterceptors(Collections.singletonList(new AllureRestTemplate())); + @ParameterizedTest + @MethodSource("clientTypeProvider") + void shouldAllowResponseBodyConsumptionAfterInterception(final SpringClientType clientType) { + final ExecutionResult executionResult = execute(clientType); + assertThat(executionResult.getResponse().getBody()).isEqualTo("some body"); + } + protected final ExecutionResult execute(final SpringClientType clientType) { final WireMockServer server = new WireMockServer(WireMockConfiguration.options().dynamicPort()); + final AtomicReference> response = new AtomicReference<>(); - return runWithinTestContext(() -> { + final AllureResults results = runWithinTestContext(() -> { server.start(); WireMock.configureFor(server.port()); WireMock.stubFor(WireMock.get(WireMock.urlEqualTo("/hello")).willReturn(WireMock.aResponse().withBody("some body"))); try { - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - HttpEntity entity = new HttpEntity<>(headers); - ResponseEntity result = restTemplate.exchange(server.url("/hello"), HttpMethod.GET, entity, String.class); + final ResponseEntity result = clientType.execute(server.url("/hello")); + response.set(result); Assertions.assertEquals(result.getStatusCode(), HttpStatus.OK); + Assertions.assertEquals(result.getBody(), "some body"); } finally { server.stop(); } }); + + return new ExecutionResult(results, response.get()); + } + + private static Attachment findAttachment(final AllureResults results, final String attachmentName) { + return results.getTestResults().stream() + .map(TestResult::getAttachments) + .flatMap(Collection::stream) + .filter(attachment -> Objects.equals(attachmentName, attachment.getName())) + .findAny() + .orElseThrow(() -> new RuntimeException("attachment not found")); + } + + private static BufferingClientHttpRequestFactory createBufferingRequestFactory() { + return new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()); + } + + private enum SpringClientType { + REST_TEMPLATE { + @Override + ResponseEntity execute(final String url) { + final RestTemplate restTemplate = new RestTemplate(createBufferingRequestFactory()); + restTemplate.setInterceptors(Collections.singletonList(new AllureRestTemplate())); + + final HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + final HttpEntity entity = new HttpEntity<>(headers); + return restTemplate.exchange(url, HttpMethod.GET, entity, String.class); + } + }, + REST_CLIENT { + @Override + ResponseEntity execute(final String url) { + final RestClient restClient = RestClient.builder() + .requestFactory(createBufferingRequestFactory()) + .requestInterceptor(new AllureRestTemplate()) + .build(); + return restClient.get() + .uri(url) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .toEntity(String.class); + } + }; + + abstract ResponseEntity execute(String url); + } + + private static final class ExecutionResult { + + private final AllureResults allureResults; + private final ResponseEntity response; + + private ExecutionResult(final AllureResults allureResults, final ResponseEntity response) { + this.allureResults = allureResults; + this.response = response; + } + + AllureResults getAllureResults() { + return allureResults; + } + + ResponseEntity getResponse() { + return response; + } } } From 35ce7c103a52bd5c1f121a47670067b0702ba61d Mon Sep 17 00:00:00 2001 From: long76 <18124433+long76@users.noreply.github.com> Date: Sun, 3 May 2026 11:02:51 +0300 Subject: [PATCH 04/13] fix run tests for junit 5.14.3 (via #1265) --- allure-hamcrest/build.gradle.kts | 1 + allure-karate/build.gradle.kts | 1 + 2 files changed, 2 insertions(+) diff --git a/allure-hamcrest/build.gradle.kts b/allure-hamcrest/build.gradle.kts index 6b0e5dd3..3817ab48 100644 --- a/allure-hamcrest/build.gradle.kts +++ b/allure-hamcrest/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { testImplementation("org.slf4j:slf4j-simple") testImplementation(project(":allure-java-commons-test")) testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") } tasks.jar { diff --git a/allure-karate/build.gradle.kts b/allure-karate/build.gradle.kts index 3a2c1ea7..18c7b4e0 100644 --- a/allure-karate/build.gradle.kts +++ b/allure-karate/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { testImplementation("org.slf4j:slf4j-simple") testImplementation(project(":allure-java-commons-test")) testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") } tasks.jar { From 46c68ad9da102ebf5a08679666bed0447d6535f0 Mon Sep 17 00:00:00 2001 From: Dmitry Baev Date: Thu, 7 May 2026 17:21:02 +0100 Subject: [PATCH 05/13] add allure report (via #1266) --- .github/workflows/build.yml | 65 ++++- AGENTS.md | 8 + .../src/test/resources/allure.properties | 1 + .../src/test/resources/allure.properties | 1 + .../src/test/resources/allure.properties | 3 + .../src/test/resources/allure.properties | 1 + .../src/test/resources/allure.properties | 1 + .../src/test/resources/allure.properties | 1 + .../src/test/resources/allure.properties | 1 + .../src/test/resources/allure.properties | 1 + .../src/test/resources/allure.properties | 1 + .../src/test/resources/allure.properties | 1 + .../src/test/resources/allure.properties | 3 + .../src/test/resources/allure.properties | 1 + .../src/test/resources/allure.properties | 1 + allure-java-commons-test/build.gradle.kts | 7 + .../allure/test/AllurePredicatesTest.java | 41 ++++ .../test/AllureResultsWriterStubTest.java | 55 +++++ .../io/qameta/allure/test/RunUtilsTest.java | 60 +++++ .../qameta/allure/test/TestUtilitiesTest.java | 65 +++++ .../src/test/resources/allure.properties | 3 + .../src/test/resources/allure.properties | 1 + .../src/test/resources/allure.properties | 1 + .../src/test/resources/allure.properties | 1 + .../src/test/resources/allure.properties | 1 + .../src/test/resources/allure.properties | 3 + .../src/test/resources/allure.properties | 1 + allure-junit-platform/build.gradle.kts | 10 + .../src/test/resources/allure.properties | 1 + allure-junit4-aspect/build.gradle.kts | 11 + .../junit4/aspect/AllureJunit4AspectTest.java | 124 ++++++++++ .../src/test/resources/allure.properties | 3 + .../src/test/resources/allure.properties | 1 + .../src/test/resources/allure.properties | 1 + .../src/test/resources/allure.properties | 1 + .../allure/karate/AllureKarateTest.java | 14 +- .../io/qameta/allure/karate/TestRunner.java | 7 + .../src/test/resources/allure.properties | 3 + .../src/test/resources/allure.properties | 3 +- .../src/test/resources/allure.properties | 1 + .../src/test/resources/allure.properties | 1 + allure-reader/build.gradle.kts | 1 + .../reader/AllureObjectMapperFactoryTest.java | 108 ++++++++ .../src/test/resources/allure.properties | 3 + .../src/test/resources/allure.properties | 1 + .../src/test/resources/allure.properties | 1 + .../src/test/resources/allure.properties | 1 + allure-servlet-api/build.gradle.kts | 9 + .../HttpServletAttachmentBuilder.java | 9 +- .../HttpServletAttachmentBuilderTest.java | 101 ++++++++ .../src/test/resources/allure.properties | 3 + .../src/test/resources/allure.properties | 1 + .../src/test/resources/allure.properties | 1 + .../src/test/resources/allure.properties | 1 + allure-test-filter/build.gradle.kts | 2 +- .../testfilter/FileTestPlanSupplierTest.java | 124 ++++++++++ .../allure/testfilter/TestPlanV1_0Test.java | 71 ++++++ .../src/test/resources/allure.properties | 3 + allure-testng/build.gradle.kts | 8 +- .../allure/testng/AllureTestNgTest.java | 231 +++++++++++------- .../src/test/resources/allure.properties | 1 + allurerc.mjs | 22 ++ build.gradle.kts | 23 +- docs/allure-agent-mode.md | 161 ++++++++++++ gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle.kts | 5 +- 66 files changed, 1291 insertions(+), 112 deletions(-) create mode 100644 AGENTS.md create mode 100644 allure-awaitility/src/test/resources/allure.properties create mode 100644 allure-hamcrest/src/test/resources/allure.properties create mode 100644 allure-java-commons-test/src/test/java/io/qameta/allure/test/AllurePredicatesTest.java create mode 100644 allure-java-commons-test/src/test/java/io/qameta/allure/test/AllureResultsWriterStubTest.java create mode 100644 allure-java-commons-test/src/test/java/io/qameta/allure/test/RunUtilsTest.java create mode 100644 allure-java-commons-test/src/test/java/io/qameta/allure/test/TestUtilitiesTest.java create mode 100644 allure-java-commons-test/src/test/resources/allure.properties create mode 100644 allure-jooq/src/test/resources/allure.properties create mode 100644 allure-junit4-aspect/src/test/java/io/qameta/allure/junit4/aspect/AllureJunit4AspectTest.java create mode 100644 allure-junit4-aspect/src/test/resources/allure.properties create mode 100644 allure-karate/src/test/resources/allure.properties create mode 100644 allure-reader/src/test/java/io/qameta/allure/reader/AllureObjectMapperFactoryTest.java create mode 100644 allure-reader/src/test/resources/allure.properties create mode 100644 allure-servlet-api/src/test/java/io/qameta/allure/servletapi/HttpServletAttachmentBuilderTest.java create mode 100644 allure-servlet-api/src/test/resources/allure.properties create mode 100644 allure-test-filter/src/test/java/io/qameta/allure/testfilter/FileTestPlanSupplierTest.java create mode 100644 allure-test-filter/src/test/java/io/qameta/allure/testfilter/TestPlanV1_0Test.java create mode 100644 allure-test-filter/src/test/resources/allure.properties create mode 100644 allurerc.mjs create mode 100644 docs/allure-agent-mode.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8f537bc9..341788f5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,22 +13,83 @@ on: - 'main' - 'hotfix-*' +concurrency: + # On main, we don't want any jobs cancelled. + # On PR branches, we cancel the job if new commits are pushed. + group: ${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + jobs: build: name: "Build" runs-on: ubuntu-latest + env: + ALLURE_MATRIX_ENV: ubuntu-jdk-21 + ALLURE_TEST_DUMP_NAME: allure-results-test-jdk-21 steps: - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: '20.x' + - name: "Set up JDK" uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: 21 + - name: "Setup Gradle" + uses: gradle/actions/setup-gradle@v6 + with: + gradle-version: 'wrapper' + - name: "Build with Gradle" run: ./gradlew build -x test --scan - - name: "Run tests" + - name: "Run tests with Allure" if: always() - run: ./gradlew --no-build-cache cleanTest test + run: npx -y allure@3 run --config ./allurerc.mjs --rerun 2 --environment="${{ env.ALLURE_MATRIX_ENV }}" --dump="${{ env.ALLURE_TEST_DUMP_NAME }}" -- ./gradlew --no-build-cache cleanTest test + + - name: "Upload Allure test dump" + if: always() + uses: actions/upload-artifact@v7 + with: + name: ${{ env.ALLURE_TEST_DUMP_NAME }} + path: ./${{ env.ALLURE_TEST_DUMP_NAME }}.zip + + report: + needs: [build] + name: "Build report" + runs-on: ubuntu-latest + if: always() + permissions: + contents: read + pull-requests: write + checks: write + env: + ALLURE_SERVICE_TOKEN: ${{ secrets.ALLURE_SERVICE_TOKEN }} + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: '20.x' + + - name: "Download Allure dumps" + uses: actions/download-artifact@v8 + continue-on-error: true + with: + pattern: allure-results-* + path: ./ + merge-multiple: true + + - name: "Generate Allure report" + run: npx -y allure@3 generate --config ./allurerc.mjs --dump="allure-results-*.zip" --output=./build/allure-report + + - name: "Post Allure summary" + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false + uses: allure-framework/allure-action@v0 + with: + report-directory: ./build/allure-report + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..8b7a6aa4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,8 @@ +# Project Guide + +Use [Allure Agent Mode](docs/allure-agent-mode.md) for all test-related work in this repository. + +- Read `docs/allure-agent-mode.md` before designing, writing, reviewing, validating, debugging, or enriching tests. +- Run test-executing commands through `allure run`, including smoke checks after small edits. +- Use `./gradlew` for repo-local test commands and scope runs to the smallest relevant module or task. +- If agent-mode output is missing or incomplete, debug that first rather than relying on console-only conclusions. diff --git a/allure-assertj/src/test/resources/allure.properties b/allure-assertj/src/test/resources/allure.properties index 9c0b0a2d..c881472e 100644 --- a/allure-assertj/src/test/resources/allure.properties +++ b/allure-assertj/src/test/resources/allure.properties @@ -1,2 +1,3 @@ allure.results.directory=build/allure-results allure.label.epic=#project.description# +allure.label.module=allure-assertj diff --git a/allure-attachments/src/test/resources/allure.properties b/allure-attachments/src/test/resources/allure.properties index 9c0b0a2d..b47a01f6 100644 --- a/allure-attachments/src/test/resources/allure.properties +++ b/allure-attachments/src/test/resources/allure.properties @@ -1,2 +1,3 @@ allure.results.directory=build/allure-results allure.label.epic=#project.description# +allure.label.module=allure-attachments diff --git a/allure-awaitility/src/test/resources/allure.properties b/allure-awaitility/src/test/resources/allure.properties new file mode 100644 index 00000000..0486d8a7 --- /dev/null +++ b/allure-awaitility/src/test/resources/allure.properties @@ -0,0 +1,3 @@ +allure.results.directory=build/allure-results +allure.label.epic=#project.description# +allure.label.module=allure-awaitility diff --git a/allure-citrus/src/test/resources/allure.properties b/allure-citrus/src/test/resources/allure.properties index 9c0b0a2d..0833b8e0 100644 --- a/allure-citrus/src/test/resources/allure.properties +++ b/allure-citrus/src/test/resources/allure.properties @@ -1,2 +1,3 @@ allure.results.directory=build/allure-results allure.label.epic=#project.description# +allure.label.module=allure-citrus diff --git a/allure-cucumber4-jvm/src/test/resources/allure.properties b/allure-cucumber4-jvm/src/test/resources/allure.properties index dbfefee4..e026a61f 100644 --- a/allure-cucumber4-jvm/src/test/resources/allure.properties +++ b/allure-cucumber4-jvm/src/test/resources/allure.properties @@ -1,3 +1,4 @@ allure.model.indentOutput=true allure.results.directory=build/allure-results allure.label.epic=#project.description# +allure.label.module=allure-cucumber4-jvm diff --git a/allure-cucumber5-jvm/src/test/resources/allure.properties b/allure-cucumber5-jvm/src/test/resources/allure.properties index dbfefee4..8e296072 100644 --- a/allure-cucumber5-jvm/src/test/resources/allure.properties +++ b/allure-cucumber5-jvm/src/test/resources/allure.properties @@ -1,3 +1,4 @@ allure.model.indentOutput=true allure.results.directory=build/allure-results allure.label.epic=#project.description# +allure.label.module=allure-cucumber5-jvm diff --git a/allure-cucumber6-jvm/src/test/resources/allure.properties b/allure-cucumber6-jvm/src/test/resources/allure.properties index dbfefee4..6b16969f 100644 --- a/allure-cucumber6-jvm/src/test/resources/allure.properties +++ b/allure-cucumber6-jvm/src/test/resources/allure.properties @@ -1,3 +1,4 @@ allure.model.indentOutput=true allure.results.directory=build/allure-results allure.label.epic=#project.description# +allure.label.module=allure-cucumber6-jvm diff --git a/allure-cucumber7-jvm/src/test/resources/allure.properties b/allure-cucumber7-jvm/src/test/resources/allure.properties index dbfefee4..20fde5c1 100644 --- a/allure-cucumber7-jvm/src/test/resources/allure.properties +++ b/allure-cucumber7-jvm/src/test/resources/allure.properties @@ -1,3 +1,4 @@ allure.model.indentOutput=true allure.results.directory=build/allure-results allure.label.epic=#project.description# +allure.label.module=allure-cucumber7-jvm diff --git a/allure-descriptions-javadoc/src/test/resources/allure.properties b/allure-descriptions-javadoc/src/test/resources/allure.properties index 9c0b0a2d..384d9cd5 100644 --- a/allure-descriptions-javadoc/src/test/resources/allure.properties +++ b/allure-descriptions-javadoc/src/test/resources/allure.properties @@ -1,2 +1,3 @@ allure.results.directory=build/allure-results allure.label.epic=#project.description# +allure.label.module=allure-descriptions-javadoc diff --git a/allure-grpc/src/test/resources/allure.properties b/allure-grpc/src/test/resources/allure.properties index 9c0b0a2d..55602943 100644 --- a/allure-grpc/src/test/resources/allure.properties +++ b/allure-grpc/src/test/resources/allure.properties @@ -1,2 +1,3 @@ allure.results.directory=build/allure-results allure.label.epic=#project.description# +allure.label.module=allure-grpc diff --git a/allure-hamcrest/src/test/resources/allure.properties b/allure-hamcrest/src/test/resources/allure.properties new file mode 100644 index 00000000..ee8c853e --- /dev/null +++ b/allure-hamcrest/src/test/resources/allure.properties @@ -0,0 +1,3 @@ +allure.results.directory=build/allure-results +allure.label.epic=#project.description# +allure.label.module=allure-hamcrest diff --git a/allure-httpclient/src/test/resources/allure.properties b/allure-httpclient/src/test/resources/allure.properties index 9c0b0a2d..0b9f016c 100644 --- a/allure-httpclient/src/test/resources/allure.properties +++ b/allure-httpclient/src/test/resources/allure.properties @@ -1,2 +1,3 @@ allure.results.directory=build/allure-results allure.label.epic=#project.description# +allure.label.module=allure-httpclient diff --git a/allure-httpclient5/src/test/resources/allure.properties b/allure-httpclient5/src/test/resources/allure.properties index 9c0b0a2d..a6feadd7 100644 --- a/allure-httpclient5/src/test/resources/allure.properties +++ b/allure-httpclient5/src/test/resources/allure.properties @@ -1,2 +1,3 @@ allure.results.directory=build/allure-results allure.label.epic=#project.description# +allure.label.module=allure-httpclient5 diff --git a/allure-java-commons-test/build.gradle.kts b/allure-java-commons-test/build.gradle.kts index e1d5c399..92c4e2e0 100644 --- a/allure-java-commons-test/build.gradle.kts +++ b/allure-java-commons-test/build.gradle.kts @@ -6,6 +6,9 @@ dependencies { api("org.apache.commons:commons-lang3") api(project(":allure-java-commons")) implementation("com.fasterxml.jackson.core:jackson-databind") + testImplementation("org.junit.jupiter:junit-jupiter-api") + testImplementation(project(":allure-junit-platform")) + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") } tasks.jar { @@ -15,3 +18,7 @@ tasks.jar { )) } } + +tasks.test { + useJUnitPlatform() +} diff --git a/allure-java-commons-test/src/test/java/io/qameta/allure/test/AllurePredicatesTest.java b/allure-java-commons-test/src/test/java/io/qameta/allure/test/AllurePredicatesTest.java new file mode 100644 index 00000000..4f5048a5 --- /dev/null +++ b/allure-java-commons-test/src/test/java/io/qameta/allure/test/AllurePredicatesTest.java @@ -0,0 +1,41 @@ +/* + * 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.test; + +import io.qameta.allure.model.Label; +import io.qameta.allure.model.Status; +import io.qameta.allure.model.TestResult; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AllurePredicatesTest { + + @Test + void shouldMatchStatusAndLabels() { + final TestResult result = new TestResult() + .setStatus(Status.PASSED) + .setLabels(List.of(new Label().setName("feature").setValue("attachments"))); + + assertTrue(AllurePredicates.hasStatus(Status.PASSED).test(result)); + assertTrue(AllurePredicates.hasLabel("feature", "attachments").test(result)); + assertFalse(AllurePredicates.hasStatus(Status.FAILED).test(result)); + assertFalse(AllurePredicates.hasLabel("feature", "steps").test(result)); + } +} diff --git a/allure-java-commons-test/src/test/java/io/qameta/allure/test/AllureResultsWriterStubTest.java b/allure-java-commons-test/src/test/java/io/qameta/allure/test/AllureResultsWriterStubTest.java new file mode 100644 index 00000000..607d7013 --- /dev/null +++ b/allure-java-commons-test/src/test/java/io/qameta/allure/test/AllureResultsWriterStubTest.java @@ -0,0 +1,55 @@ +/* + * 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.test; + +import io.qameta.allure.Allure; +import io.qameta.allure.model.TestResult; +import io.qameta.allure.model.TestResultContainer; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +class AllureResultsWriterStubTest { + + @Test + void shouldStoreResultsContainersAndAttachments() { + final AllureResultsWriterStub writer = new AllureResultsWriterStub(); + final TestResult testResult = new TestResult() + .setUuid("test-uuid") + .setName("demo"); + final TestResultContainer container = new TestResultContainer() + .setUuid("container-uuid") + .setChildren(List.of("test-uuid")); + + Allure.step("Store a test result, its container, and an attachment", () -> { + writer.write(testResult); + writer.write(container); + writer.write("payload.txt", new ByteArrayInputStream("payload".getBytes(StandardCharsets.UTF_8))); + }); + + Allure.step("Verify the stub exposes the written runtime artifacts", () -> { + assertSame(testResult, writer.getTestResultByName("demo")); + assertEquals(List.of(container), writer.getTestResultContainersForTestResult(testResult)); + assertArrayEquals("payload".getBytes(StandardCharsets.UTF_8), writer.getAttachments().get("payload.txt")); + }); + } +} diff --git a/allure-java-commons-test/src/test/java/io/qameta/allure/test/RunUtilsTest.java b/allure-java-commons-test/src/test/java/io/qameta/allure/test/RunUtilsTest.java new file mode 100644 index 00000000..12bd9c5d --- /dev/null +++ b/allure-java-commons-test/src/test/java/io/qameta/allure/test/RunUtilsTest.java @@ -0,0 +1,60 @@ +/* + * 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.test; + +import io.qameta.allure.Allure; +import io.qameta.allure.model.Status; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RunUtilsTest { + + @Test + void shouldCaptureFailureStatusWithinSyntheticTestContext() { + final AllureResults results = Allure.step("Execute a synthetic test context that raises an assertion error", () -> + RunUtils.runWithinTestContext(() -> { + throw new AssertionError("boom"); + }) + ); + + Allure.step("Verify the captured synthetic test result is marked as failed", () -> { + assertEquals(1, results.getTestResults().size()); + assertEquals(Status.FAILED, results.getTestResults().get(0).getStatus()); + assertTrue(results.getTestResults().get(0).getStatusDetails().getMessage().contains("boom")); + }); + } + + @Test + void shouldAttachNestedRunArtifactsToOuterLifecycle() { + final AllureResults results = Allure.step("Execute a nested synthetic run and capture its emitted attachments", () -> + RunUtils.runWithinTestContext(() -> + RunUtils.runWithinTestContext(() -> { + }) + ) + ); + + Allure.addAttachment("nested-attachment-keys", String.join("\n", results.getAttachments().keySet())); + Allure.step("Verify the outer lifecycle receives serialized artifacts from the nested run", () -> { + assertFalse(results.getAttachments().isEmpty()); + assertTrue(results.getAttachments().values().stream() + .map(bytes -> new String(bytes, java.nio.charset.StandardCharsets.UTF_8)) + .anyMatch(body -> body.contains("\"uuid\""))); + }); + } +} diff --git a/allure-java-commons-test/src/test/java/io/qameta/allure/test/TestUtilitiesTest.java b/allure-java-commons-test/src/test/java/io/qameta/allure/test/TestUtilitiesTest.java new file mode 100644 index 00000000..d7fb298d --- /dev/null +++ b/allure-java-commons-test/src/test/java/io/qameta/allure/test/TestUtilitiesTest.java @@ -0,0 +1,65 @@ +/* + * 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.test; + +import io.qameta.allure.Allure; +import io.github.benas.randombeans.api.EnhancedRandom; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TestUtilitiesTest { + + @Test + void shouldGenerateStableThreadLocalRandomPerThread() throws Exception { + final EnhancedRandom mainThread = ThreadLocalEnhancedRandom.current(); + final AtomicReference workerThread = new AtomicReference<>(); + final Thread thread = new Thread(() -> + workerThread.set(ThreadLocalEnhancedRandom.current()) + ); + + Allure.step("Resolve thread-local random generators on two threads and compare their identities", () -> { + thread.start(); + thread.join(); + Allure.addAttachment( + "thread-local-random-identities", + "main=" + System.identityHashCode(mainThread) + + "\nworker=" + System.identityHashCode(workerThread.get()) + ); + assertSame(mainThread, ThreadLocalEnhancedRandom.current()); + assertNotSame(mainThread, workerThread.get()); + }); + } + + @Test + void shouldGenerateExpectedRandomTestDataShapes() { + final String name = TestData.randomName(); + final String id = TestData.randomId(); + final String value = TestData.randomString(16); + + assertEquals(10, name.length()); + assertEquals(10, id.length()); + assertEquals(16, value.length()); + assertTrue(name.matches("[A-Za-z]+")); + assertTrue(id.matches("[A-Za-z0-9]+")); + assertTrue(value.matches("[A-Za-z0-9]+")); + } +} diff --git a/allure-java-commons-test/src/test/resources/allure.properties b/allure-java-commons-test/src/test/resources/allure.properties new file mode 100644 index 00000000..c1b2f8a0 --- /dev/null +++ b/allure-java-commons-test/src/test/resources/allure.properties @@ -0,0 +1,3 @@ +allure.results.directory=build/allure-results +allure.label.epic=#project.description# +allure.label.module=allure-java-commons-test diff --git a/allure-java-commons/src/test/resources/allure.properties b/allure-java-commons/src/test/resources/allure.properties index e4f1d9fc..9f941c45 100644 --- a/allure-java-commons/src/test/resources/allure.properties +++ b/allure-java-commons/src/test/resources/allure.properties @@ -1,3 +1,4 @@ allure.results.directory=build/allure-results allure.link.issue.pattern=https://github.com/allure-framework/allure-java/issues/{} allure.label.epic=#project.description# +allure.label.module=allure-java-commons diff --git a/allure-jax-rs/src/test/resources/allure.properties b/allure-jax-rs/src/test/resources/allure.properties index 9c0b0a2d..cd2d7056 100644 --- a/allure-jax-rs/src/test/resources/allure.properties +++ b/allure-jax-rs/src/test/resources/allure.properties @@ -1,2 +1,3 @@ allure.results.directory=build/allure-results allure.label.epic=#project.description# +allure.label.module=allure-jax-rs diff --git a/allure-jbehave/src/test/resources/allure.properties b/allure-jbehave/src/test/resources/allure.properties index 9c0b0a2d..04f5f5b6 100644 --- a/allure-jbehave/src/test/resources/allure.properties +++ b/allure-jbehave/src/test/resources/allure.properties @@ -1,2 +1,3 @@ allure.results.directory=build/allure-results allure.label.epic=#project.description# +allure.label.module=allure-jbehave diff --git a/allure-jbehave5/src/test/resources/allure.properties b/allure-jbehave5/src/test/resources/allure.properties index 9c0b0a2d..d61e9c1d 100644 --- a/allure-jbehave5/src/test/resources/allure.properties +++ b/allure-jbehave5/src/test/resources/allure.properties @@ -1,2 +1,3 @@ allure.results.directory=build/allure-results allure.label.epic=#project.description# +allure.label.module=allure-jbehave5 diff --git a/allure-jooq/src/test/resources/allure.properties b/allure-jooq/src/test/resources/allure.properties new file mode 100644 index 00000000..d86adbae --- /dev/null +++ b/allure-jooq/src/test/resources/allure.properties @@ -0,0 +1,3 @@ +allure.results.directory=build/allure-results +allure.label.epic=#project.description# +allure.label.module=allure-jooq diff --git a/allure-jsonunit/src/test/resources/allure.properties b/allure-jsonunit/src/test/resources/allure.properties index 9c0b0a2d..5f86fe63 100644 --- a/allure-jsonunit/src/test/resources/allure.properties +++ b/allure-jsonunit/src/test/resources/allure.properties @@ -1,2 +1,3 @@ allure.results.directory=build/allure-results allure.label.epic=#project.description# +allure.label.module=allure-jsonunit diff --git a/allure-junit-platform/build.gradle.kts b/allure-junit-platform/build.gradle.kts index 4cf244bf..b090e9ec 100644 --- a/allure-junit-platform/build.gradle.kts +++ b/allure-junit-platform/build.gradle.kts @@ -29,11 +29,21 @@ tasks.jar { } tasks.test { + // The Allure Gradle adapter adds this module's published artifact to the + // test runtime classpath, so make the jar/task relationship explicit when + // jar and test are scheduled in the same build. + dependsOn(tasks.jar) systemProperty("junit.jupiter.execution.parallel.enabled", "false") useJUnitPlatform() exclude("**/features/*") } +tasks.named("pmdMain") { + // PMD type resolution reads the main compile classpath, which also + // contains this module's published artifact via the Allure adapter setup. + dependsOn(tasks.jar) +} + val spiOffJar: Jar by tasks.creating(Jar::class) { from(sourceSets.getByName("main").output) archiveClassifier.set("spi-off") diff --git a/allure-junit-platform/src/test/resources/allure.properties b/allure-junit-platform/src/test/resources/allure.properties index 9c0b0a2d..cff9d085 100644 --- a/allure-junit-platform/src/test/resources/allure.properties +++ b/allure-junit-platform/src/test/resources/allure.properties @@ -1,2 +1,3 @@ allure.results.directory=build/allure-results allure.label.epic=#project.description# +allure.label.module=allure-junit-platform diff --git a/allure-junit4-aspect/build.gradle.kts b/allure-junit4-aspect/build.gradle.kts index 79d3bc53..df743502 100644 --- a/allure-junit4-aspect/build.gradle.kts +++ b/allure-junit4-aspect/build.gradle.kts @@ -6,6 +6,13 @@ dependencies { api(project(":allure-junit4")) compileOnly("junit:junit:$junitVersion") compileOnly("org.aspectj:aspectjrt") + testImplementation("junit:junit:$junitVersion") + testImplementation("org.junit.jupiter:junit-jupiter-api") + testImplementation("org.aspectj:aspectjrt") + testImplementation("org.mockito:mockito-core") + testImplementation("org.slf4j:slf4j-simple") + testImplementation(project(":allure-junit-platform")) + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") } tasks.jar { @@ -15,3 +22,7 @@ tasks.jar { )) } } + +tasks.test { + useJUnitPlatform() +} diff --git a/allure-junit4-aspect/src/test/java/io/qameta/allure/junit4/aspect/AllureJunit4AspectTest.java b/allure-junit4-aspect/src/test/java/io/qameta/allure/junit4/aspect/AllureJunit4AspectTest.java new file mode 100644 index 00000000..d76ba207 --- /dev/null +++ b/allure-junit4-aspect/src/test/java/io/qameta/allure/junit4/aspect/AllureJunit4AspectTest.java @@ -0,0 +1,124 @@ +/* + * 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.junit4.aspect; + +import io.qameta.allure.Allure; +import io.qameta.allure.junit4.AllureJunit4; +import io.qameta.allure.junit4.AllureJunit4Filter; +import org.aspectj.lang.JoinPoint; +import org.junit.jupiter.api.Test; +import org.junit.runner.Description; +import org.junit.runner.Runner; +import org.junit.runner.manipulation.Filter; +import org.junit.runner.manipulation.Filterable; +import org.junit.runner.manipulation.NoTestsRemainException; +import org.junit.runner.notification.RunListener; +import org.junit.runner.notification.RunNotifier; + +import java.lang.reflect.Field; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class AllureJunit4AspectTest { + + @Test + void shouldApplyAllureFilterToFilterableRunner() { + final TrackingRunner runner = new TrackingRunner(false); + + new AllureJunit4FilterAspect().filterBeforeRun(runner); + + assertNotNull(runner.appliedFilter); + assertInstanceOf(AllureJunit4Filter.class, runner.appliedFilter); + } + + @Test + void shouldIgnoreNoTestsRemainExceptionDuringFiltering() { + final TrackingRunner runner = new TrackingRunner(true); + + assertDoesNotThrow(() -> new AllureJunit4FilterAspect().filterBeforeRun(runner)); + assertNotNull(runner.appliedFilter); + } + + @Test + void shouldAddListenerToPlainRunNotifier() throws Exception { + final RunNotifier notifier = new RunNotifier(); + final JoinPoint point = mock(JoinPoint.class); + when(point.getThis()).thenReturn(notifier); + + Allure.step("Invoke the listener aspect against a plain RunNotifier", () -> + new AllureJunit4ListenerAspect().addListener(point) + ); + + Allure.step("Verify the notifier now contains the Allure JUnit 4 listener", () -> + assertTrue(getListeners(notifier).stream().anyMatch(AllureJunit4.class::isInstance)) + ); + } + + @Test + void shouldSkipDerivedRunNotifierInstances() throws Exception { + final DerivedRunNotifier notifier = new DerivedRunNotifier(); + final JoinPoint point = mock(JoinPoint.class); + when(point.getThis()).thenReturn(notifier); + + new AllureJunit4ListenerAspect().addListener(point); + + assertFalse(getListeners(notifier).stream().anyMatch(AllureJunit4.class::isInstance)); + } + + @SuppressWarnings("unchecked") + private static List getListeners(final RunNotifier notifier) throws Exception { + final Field listeners = RunNotifier.class.getDeclaredField("listeners"); + listeners.setAccessible(true); + return (List) listeners.get(notifier); + } + + private static final class TrackingRunner extends Runner implements Filterable { + private final boolean throwNoTestsRemain; + private Filter appliedFilter; + + private TrackingRunner(final boolean throwNoTestsRemain) { + this.throwNoTestsRemain = throwNoTestsRemain; + } + + @Override + public Description getDescription() { + return Description.EMPTY; + } + + @Override + public void run(final RunNotifier notifier) { + // no-op + } + + @Override + public void filter(final Filter filter) throws NoTestsRemainException { + this.appliedFilter = filter; + if (throwNoTestsRemain) { + throw new NoTestsRemainException(); + } + } + } + + private static final class DerivedRunNotifier extends RunNotifier { + } +} diff --git a/allure-junit4-aspect/src/test/resources/allure.properties b/allure-junit4-aspect/src/test/resources/allure.properties new file mode 100644 index 00000000..3d37acf1 --- /dev/null +++ b/allure-junit4-aspect/src/test/resources/allure.properties @@ -0,0 +1,3 @@ +allure.results.directory=build/allure-results +allure.label.epic=#project.description# +allure.label.module=allure-junit4-aspect diff --git a/allure-junit4/src/test/resources/allure.properties b/allure-junit4/src/test/resources/allure.properties index 9c0b0a2d..b68758d1 100644 --- a/allure-junit4/src/test/resources/allure.properties +++ b/allure-junit4/src/test/resources/allure.properties @@ -1,2 +1,3 @@ allure.results.directory=build/allure-results allure.label.epic=#project.description# +allure.label.module=allure-junit4 diff --git a/allure-jupiter-assert/src/test/resources/allure.properties b/allure-jupiter-assert/src/test/resources/allure.properties index 9c0b0a2d..2f82d5fa 100644 --- a/allure-jupiter-assert/src/test/resources/allure.properties +++ b/allure-jupiter-assert/src/test/resources/allure.properties @@ -1,2 +1,3 @@ allure.results.directory=build/allure-results allure.label.epic=#project.description# +allure.label.module=allure-jupiter-assert diff --git a/allure-jupiter/src/test/resources/allure.properties b/allure-jupiter/src/test/resources/allure.properties index 4c9c3b8e..36ea007c 100644 --- a/allure-jupiter/src/test/resources/allure.properties +++ b/allure-jupiter/src/test/resources/allure.properties @@ -1,3 +1,4 @@ allure.results.directory=build/allure-results allure.label.epic=#project.description# allure.link.issue.pattern=https://github.com/allure-framework/allure-java/issues/{} +allure.label.module=allure-jupiter diff --git a/allure-karate/src/test/java/io/qameta/allure/karate/AllureKarateTest.java b/allure-karate/src/test/java/io/qameta/allure/karate/AllureKarateTest.java index 5d18e9f0..5dc65ac0 100644 --- a/allure-karate/src/test/java/io/qameta/allure/karate/AllureKarateTest.java +++ b/allure-karate/src/test/java/io/qameta/allure/karate/AllureKarateTest.java @@ -16,6 +16,8 @@ package io.qameta.allure.karate; import com.intuit.karate.Runner; +import io.qameta.allure.AllureLifecycle; +import io.qameta.allure.FileSystemResultsWriter; import io.qameta.allure.model.Label; import io.qameta.allure.model.Link; import io.qameta.allure.model.Parameter; @@ -26,6 +28,8 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import java.nio.file.Path; + import static io.qameta.allure.model.Status.BROKEN; import static io.qameta.allure.model.Status.PASSED; import static io.qameta.allure.util.ResultsUtils.md5; @@ -334,13 +338,21 @@ void shouldSkipCallAndCallOnceStepsInBeforeStep() { @Test void buildTest() { + final Path allureResults = temp.resolve("allure-results"); + Runner.builder() .path("classpath:testdata/greeting.feature") - .hook(new AllureKarate()) + .hook(new AllureKarate(new AllureLifecycle( + new FileSystemResultsWriter(allureResults) + ))) .backupReportDir(false) + .reportDir(temp.resolve("karate-reports").toString()) .outputJunitXml(false) .outputCucumberJson(false) .outputHtmlReport(false) .parallel(1); + + assertThat(allureResults) + .isDirectoryContaining(path -> path.getFileName().toString().endsWith("-result.json")); } } diff --git a/allure-karate/src/test/java/io/qameta/allure/karate/TestRunner.java b/allure-karate/src/test/java/io/qameta/allure/karate/TestRunner.java index 1149317c..677e5757 100644 --- a/allure-karate/src/test/java/io/qameta/allure/karate/TestRunner.java +++ b/allure-karate/src/test/java/io/qameta/allure/karate/TestRunner.java @@ -17,11 +17,14 @@ import io.qameta.allure.test.AllureResults; import io.qameta.allure.test.RunUtils; +import org.junit.jupiter.api.io.TempDir; import org.mockserver.client.MockServerClient; import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; import org.mockserver.netty.MockServer; +import java.nio.file.Path; + import static org.mockserver.model.HttpRequest.request; import static org.mockserver.model.HttpResponse.response; @@ -31,6 +34,9 @@ public class TestRunner { private MockServer server; private MockServerClient client; + @TempDir + protected Path temp; + AllureResults runApi(final String... featurePath) { server = new MockServer(8081); client = new MockServerClient("localhost", server.getLocalPort()); @@ -63,6 +69,7 @@ AllureResults run(final String... path) { .path(path) .hook(allureKarate) .backupReportDir(false) + .reportDir(temp.resolve("karate-reports").toString()) .outputJunitXml(false) .outputCucumberJson(false) .outputHtmlReport(false) diff --git a/allure-karate/src/test/resources/allure.properties b/allure-karate/src/test/resources/allure.properties new file mode 100644 index 00000000..a4cd2a54 --- /dev/null +++ b/allure-karate/src/test/resources/allure.properties @@ -0,0 +1,3 @@ +allure.results.directory=build/allure-results +allure.label.epic=#project.description# +allure.label.module=allure-karate diff --git a/allure-model/src/test/resources/allure.properties b/allure-model/src/test/resources/allure.properties index cb77d0a3..5002f01c 100644 --- a/allure-model/src/test/resources/allure.properties +++ b/allure-model/src/test/resources/allure.properties @@ -1 +1,2 @@ -allure.results.directory=build/allure-results \ No newline at end of file +allure.results.directory=build/allure-results +allure.label.module=allure-model diff --git a/allure-okhttp/src/test/resources/allure.properties b/allure-okhttp/src/test/resources/allure.properties index 9c0b0a2d..2182ac90 100644 --- a/allure-okhttp/src/test/resources/allure.properties +++ b/allure-okhttp/src/test/resources/allure.properties @@ -1,2 +1,3 @@ allure.results.directory=build/allure-results allure.label.epic=#project.description# +allure.label.module=allure-okhttp diff --git a/allure-okhttp3/src/test/resources/allure.properties b/allure-okhttp3/src/test/resources/allure.properties index 9c0b0a2d..a835d79e 100644 --- a/allure-okhttp3/src/test/resources/allure.properties +++ b/allure-okhttp3/src/test/resources/allure.properties @@ -1,2 +1,3 @@ allure.results.directory=build/allure-results allure.label.epic=#project.description# +allure.label.module=allure-okhttp3 diff --git a/allure-reader/build.gradle.kts b/allure-reader/build.gradle.kts index 432fd815..6d05533e 100644 --- a/allure-reader/build.gradle.kts +++ b/allure-reader/build.gradle.kts @@ -7,6 +7,7 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter-api") testImplementation("org.mockito:mockito-core") testImplementation("org.slf4j:slf4j-simple") + testImplementation(project(":allure-junit-platform")) testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") } diff --git a/allure-reader/src/test/java/io/qameta/allure/reader/AllureObjectMapperFactoryTest.java b/allure-reader/src/test/java/io/qameta/allure/reader/AllureObjectMapperFactoryTest.java new file mode 100644 index 00000000..74802ce1 --- /dev/null +++ b/allure-reader/src/test/java/io/qameta/allure/reader/AllureObjectMapperFactoryTest.java @@ -0,0 +1,108 @@ +/* + * 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.reader; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.qameta.allure.Allure; +import io.qameta.allure.model.Parameter; +import io.qameta.allure.model.Stage; +import io.qameta.allure.model.Status; +import io.qameta.allure.model.TestResult; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class AllureObjectMapperFactoryTest { + + @Test + void shouldCreateMapperThatReadsEnumsCaseInsensitively() throws Exception { + final TestResult result = Allure.step("Deserialize a mixed-case payload with extra fields", () -> { + final ObjectMapper mapper = AllureObjectMapperFactory.createMapper(); + return mapper.readValue( + "{" + + "\"name\":\"demo\"," + + "\"status\":\"pAsSeD\"," + + "\"stage\":\"fInIsHeD\"," + + "\"parameters\":[{\"name\":\"secret\",\"value\":\"42\",\"mode\":\"MaSkEd\"}]," + + "\"unknown\":\"ignored\"" + + "}", + TestResult.class + ); + }); + + Allure.step("Verify the mapper normalizes enums and keeps the expected payload", () -> { + assertEquals("demo", result.getName()); + assertEquals(Status.PASSED, result.getStatus()); + assertEquals(Stage.FINISHED, result.getStage()); + assertEquals(Parameter.Mode.MASKED, result.getParameters().get(0).getMode()); + }); + } + + @Test + void deprecatedDeserializersShouldTrimInputAndReturnNullForUnknownValues() throws Exception { + final ObjectMapper mapper = AllureObjectMapperFactory.createMapper(); + + final DeprecatedEnumHolder trimmed = mapper.readValue( + "{" + + "\"status\":\" broken \"," + + "\"stage\":\" pending \"," + + "\"mode\":\" hidden \"" + + "}", + DeprecatedEnumHolder.class + ); + final DeprecatedEnumHolder unknown = mapper.readValue( + "{" + + "\"status\":\"not-a-status\"," + + "\"stage\":\" \"," + + "\"mode\":\"not-a-mode\"" + + "}", + DeprecatedEnumHolder.class + ); + + Allure.step("Compare deprecated deserializer output for trimmed and unknown enum values", () -> { + Allure.addAttachment( + "enum-deserialization-summary", + "trimmed.status=" + trimmed.status + + "\ntrimmed.stage=" + trimmed.stage + + "\ntrimmed.mode=" + trimmed.mode + + "\nunknown.status=" + unknown.status + + "\nunknown.stage=" + unknown.stage + + "\nunknown.mode=" + unknown.mode + ); + assertEquals(Status.BROKEN, trimmed.status); + assertEquals(Stage.PENDING, trimmed.stage); + assertEquals(Parameter.Mode.HIDDEN, trimmed.mode); + + assertNull(unknown.status); + assertNull(unknown.stage); + assertNull(unknown.mode); + }); + } + + private static final class DeprecatedEnumHolder { + + @JsonDeserialize(using = StatusDeserializer.class) + private Status status; + + @JsonDeserialize(using = StageDeserializer.class) + private Stage stage; + + @JsonDeserialize(using = ParameterModeDeserializer.class) + private Parameter.Mode mode; + } +} diff --git a/allure-reader/src/test/resources/allure.properties b/allure-reader/src/test/resources/allure.properties new file mode 100644 index 00000000..364aa6de --- /dev/null +++ b/allure-reader/src/test/resources/allure.properties @@ -0,0 +1,3 @@ +allure.results.directory=build/allure-results +allure.label.epic=#project.description# +allure.label.module=allure-reader diff --git a/allure-rest-assured/src/test/resources/allure.properties b/allure-rest-assured/src/test/resources/allure.properties index 9c0b0a2d..89b92e23 100644 --- a/allure-rest-assured/src/test/resources/allure.properties +++ b/allure-rest-assured/src/test/resources/allure.properties @@ -1,2 +1,3 @@ allure.results.directory=build/allure-results allure.label.epic=#project.description# +allure.label.module=allure-rest-assured diff --git a/allure-scalatest/src/test/resources/allure.properties b/allure-scalatest/src/test/resources/allure.properties index 9c0b0a2d..760733e9 100644 --- a/allure-scalatest/src/test/resources/allure.properties +++ b/allure-scalatest/src/test/resources/allure.properties @@ -1,2 +1,3 @@ allure.results.directory=build/allure-results allure.label.epic=#project.description# +allure.label.module=allure-scalatest diff --git a/allure-selenide/src/test/resources/allure.properties b/allure-selenide/src/test/resources/allure.properties index 9c0b0a2d..cfcad4a8 100644 --- a/allure-selenide/src/test/resources/allure.properties +++ b/allure-selenide/src/test/resources/allure.properties @@ -1,2 +1,3 @@ allure.results.directory=build/allure-results allure.label.epic=#project.description# +allure.label.module=allure-selenide diff --git a/allure-servlet-api/build.gradle.kts b/allure-servlet-api/build.gradle.kts index 55c4bdcd..52c92821 100644 --- a/allure-servlet-api/build.gradle.kts +++ b/allure-servlet-api/build.gradle.kts @@ -6,6 +6,11 @@ dependencies { api(project(":allure-attachments")) compileOnly("javax.servlet:javax.servlet-api:$servletApiVersion") testImplementation("javax.servlet:javax.servlet-api:$servletApiVersion") + testImplementation("org.junit.jupiter:junit-jupiter-api") + testImplementation("org.mockito:mockito-core") + testImplementation("org.slf4j:slf4j-simple") + testImplementation(project(":allure-junit-platform")) + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") } tasks.jar { @@ -15,3 +20,7 @@ tasks.jar { )) } } + +tasks.test { + useJUnitPlatform() +} diff --git a/allure-servlet-api/src/main/java/io/qameta/allure/servletapi/HttpServletAttachmentBuilder.java b/allure-servlet-api/src/main/java/io/qameta/allure/servletapi/HttpServletAttachmentBuilder.java index fe1c3abc..314667b1 100644 --- a/allure-servlet-api/src/main/java/io/qameta/allure/servletapi/HttpServletAttachmentBuilder.java +++ b/allure-servlet-api/src/main/java/io/qameta/allure/servletapi/HttpServletAttachmentBuilder.java @@ -24,8 +24,8 @@ import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.IOException; +import java.util.Arrays; import java.util.Collections; -import java.util.stream.Stream; import static io.qameta.allure.attachment.http.HttpRequestAttachment.Builder.create; import static io.qameta.allure.attachment.http.HttpResponseAttachment.Builder.create; @@ -49,8 +49,11 @@ public static HttpRequestAttachment buildRequest(final HttpServletRequest reques requestBuilder.setHeader(name, value); }); - Stream.of(request.getCookies()) - .forEach(cookie -> requestBuilder.setCookie(cookie.getName(), cookie.getValue())); + final javax.servlet.http.Cookie[] cookies = request.getCookies(); + if (cookies != null) { + Arrays.stream(cookies) + .forEach(cookie -> requestBuilder.setCookie(cookie.getName(), cookie.getValue())); + } requestBuilder.setBody(getBody(request)); return requestBuilder.build(); } diff --git a/allure-servlet-api/src/test/java/io/qameta/allure/servletapi/HttpServletAttachmentBuilderTest.java b/allure-servlet-api/src/test/java/io/qameta/allure/servletapi/HttpServletAttachmentBuilderTest.java new file mode 100644 index 00000000..4fca9bfc --- /dev/null +++ b/allure-servlet-api/src/test/java/io/qameta/allure/servletapi/HttpServletAttachmentBuilderTest.java @@ -0,0 +1,101 @@ +/* + * 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.servletapi; + +import io.qameta.allure.Allure; +import io.qameta.allure.attachment.http.HttpRequestAttachment; +import io.qameta.allure.attachment.http.HttpResponseAttachment; +import org.junit.jupiter.api.Test; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class HttpServletAttachmentBuilderTest { + + @Test + void shouldBuildRequestWithHeadersCookiesAndBody() throws Exception { + final HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRequestURI()).thenReturn("/orders"); + when(request.getHeaderNames()).thenReturn(Collections.enumeration(List.of("X-Trace", "Accept"))); + when(request.getHeader("X-Trace")).thenReturn("trace-1"); + when(request.getHeader("Accept")).thenReturn("application/json"); + when(request.getCookies()).thenReturn(new Cookie[]{new Cookie("session", "abc123")}); + when(request.getReader()).thenReturn(new BufferedReader(new StringReader("{\"ok\":true}"))); + + final HttpRequestAttachment attachment = HttpServletAttachmentBuilder.buildRequest(request); + + assertEquals("Request", attachment.getName()); + assertEquals("/orders", attachment.getUrl()); + assertEquals("{\"ok\":true}", attachment.getBody()); + assertEquals(Map.of("X-Trace", "trace-1", "Accept", "application/json"), attachment.getHeaders()); + assertEquals(Map.of("session", "abc123"), attachment.getCookies()); + } + + @Test + void shouldHandleRequestsWithoutCookies() throws Exception { + final HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRequestURI()).thenReturn("/orders"); + when(request.getHeaderNames()).thenReturn(Collections.emptyEnumeration()); + when(request.getCookies()).thenReturn(null); + when(request.getReader()).thenReturn(new BufferedReader(new StringReader(""))); + + final HttpRequestAttachment attachment = Allure.step( + "Build a request attachment when the servlet container returns null cookies", + () -> assertDoesNotThrow(() -> HttpServletAttachmentBuilder.buildRequest(request)) + ); + + Allure.step("Verify the request attachment keeps an empty cookie map", () -> + assertTrue(attachment.getCookies().isEmpty()) + ); + } + + @Test + void shouldBuildResponseWithHeaders() { + final HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getHeaderNames()).thenReturn(List.of("Content-Type")); + when(response.getHeaders("Content-Type")).thenReturn(List.of("application/json")); + + final HttpResponseAttachment attachment = HttpServletAttachmentBuilder.buildResponse(response); + + assertEquals("Response", attachment.getName()); + assertEquals(Map.of("Content-Type", "application/json"), attachment.getHeaders()); + } + + @Test + void shouldReturnEmptyBodyWhenRequestReaderFails() throws Exception { + final HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getReader()).thenThrow(new IOException("boom")); + + final String body = Allure.step("Read the request body when the servlet reader throws", () -> + HttpServletAttachmentBuilder.getBody(request) + ); + + Allure.step("Verify the fallback body is empty", () -> assertEquals("", body)); + } +} diff --git a/allure-servlet-api/src/test/resources/allure.properties b/allure-servlet-api/src/test/resources/allure.properties new file mode 100644 index 00000000..8f58f812 --- /dev/null +++ b/allure-servlet-api/src/test/resources/allure.properties @@ -0,0 +1,3 @@ +allure.results.directory=build/allure-results +allure.label.epic=#project.description# +allure.label.module=allure-servlet-api diff --git a/allure-spock/src/test/resources/allure.properties b/allure-spock/src/test/resources/allure.properties index 9c0b0a2d..92d17a33 100644 --- a/allure-spock/src/test/resources/allure.properties +++ b/allure-spock/src/test/resources/allure.properties @@ -1,2 +1,3 @@ allure.results.directory=build/allure-results allure.label.epic=#project.description# +allure.label.module=allure-spock diff --git a/allure-spock2/src/test/resources/allure.properties b/allure-spock2/src/test/resources/allure.properties index 9c0b0a2d..19503db3 100644 --- a/allure-spock2/src/test/resources/allure.properties +++ b/allure-spock2/src/test/resources/allure.properties @@ -1,2 +1,3 @@ allure.results.directory=build/allure-results allure.label.epic=#project.description# +allure.label.module=allure-spock2 diff --git a/allure-spring-web/src/test/resources/allure.properties b/allure-spring-web/src/test/resources/allure.properties index 9c0b0a2d..594c44e1 100644 --- a/allure-spring-web/src/test/resources/allure.properties +++ b/allure-spring-web/src/test/resources/allure.properties @@ -1,2 +1,3 @@ allure.results.directory=build/allure-results allure.label.epic=#project.description# +allure.label.module=allure-spring-web diff --git a/allure-test-filter/build.gradle.kts b/allure-test-filter/build.gradle.kts index b2198ce3..2c96987f 100644 --- a/allure-test-filter/build.gradle.kts +++ b/allure-test-filter/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { testImplementation("org.slf4j:slf4j-simple") testImplementation(project(":allure-assertj")) testImplementation(project(":allure-java-commons-test")) + testImplementation(project(":allure-junit-platform")) testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") } @@ -68,4 +69,3 @@ tasks { exclude("**/features/*") } } - diff --git a/allure-test-filter/src/test/java/io/qameta/allure/testfilter/FileTestPlanSupplierTest.java b/allure-test-filter/src/test/java/io/qameta/allure/testfilter/FileTestPlanSupplierTest.java new file mode 100644 index 00000000..dda3d2de --- /dev/null +++ b/allure-test-filter/src/test/java/io/qameta/allure/testfilter/FileTestPlanSupplierTest.java @@ -0,0 +1,124 @@ +/* + * 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.testfilter; + +import io.qameta.allure.Allure; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class FileTestPlanSupplierTest { + + @Test + void shouldReadPlanFromPrimaryEnvironmentVariable() throws Exception { + final ProbeResult result = Allure.step("Run the supplier probe with ALLURE_TESTPLAN_PATH", () -> + runProbe("ALLURE_TESTPLAN_PATH") + ); + + recordProbe(result); + Allure.step("Verify the probe selects the configured test plan entry", () -> { + assertEquals(0, result.exitCode); + assertEquals("selected=true", result.stdout.strip()); + }); + } + + @Test + void shouldReadPlanFromLegacyEnvironmentVariable() throws Exception { + final ProbeResult result = Allure.step("Run the supplier probe with AS_TESTPLAN_PATH", () -> + runProbe("AS_TESTPLAN_PATH") + ); + + recordProbe(result); + Allure.step("Verify the legacy environment alias still loads the plan", () -> { + assertEquals(0, result.exitCode); + assertEquals("selected=true", result.stdout.strip()); + }); + } + + private ProbeResult runProbe(final String variableName) throws IOException, InterruptedException { + final Path plan = Files.createTempFile("allure-testplan-", ".json"); + Files.writeString( + plan, + "{" + + "\"version\":\"1.0\"," + + "\"tests\":[{\"id\":\"A-1\",\"selector\":\"pkg.Test#name\"}]" + + "}", + StandardCharsets.UTF_8 + ); + + final String javaBin = Path.of(System.getProperty("java.home"), "bin", "java").toString(); + final ProcessBuilder builder = new ProcessBuilder( + javaBin, + "-cp", + System.getProperty("java.class.path"), + SupplierProbe.class.getName() + ); + final Map environment = builder.environment(); + environment.remove("ALLURE_TESTPLAN_PATH"); + environment.remove("AS_TESTPLAN_PATH"); + environment.put(variableName, plan.toString()); + + final Process process = builder.start(); + final boolean finished = process.waitFor(Duration.ofSeconds(10).toMillis(), java.util.concurrent.TimeUnit.MILLISECONDS); + if (!finished) { + process.destroyForcibly(); + throw new IllegalStateException("supplier probe timed out"); + } + + final String stdout = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + final String stderr = new String(process.getErrorStream().readAllBytes(), StandardCharsets.UTF_8); + return new ProbeResult(process.exitValue(), stdout, stderr); + } + + private void recordProbe(final ProbeResult result) { + Allure.addAttachment("probe-stdout", result.stdout); + Allure.addAttachment("probe-stderr", result.stderr); + } + + private static final class ProbeResult { + private final int exitCode; + private final String stdout; + private final String stderr; + + private ProbeResult(final int exitCode, final String stdout, final String stderr) { + this.exitCode = exitCode; + this.stdout = stdout; + this.stderr = stderr; + } + } + + public static final class SupplierProbe { + private SupplierProbe() { + } + + public static void main(final String[] args) { + final FileTestPlanSupplier supplier = new FileTestPlanSupplier(); + final String result = supplier.supply() + .filter(TestPlanV1_0.class::isInstance) + .map(TestPlanV1_0.class::cast) + .map(plan -> "selected=" + plan.isSelected("A-1", "pkg.Test#name")) + .orElse("selected=false"); + System.out.println(result); + } + } +} diff --git a/allure-test-filter/src/test/java/io/qameta/allure/testfilter/TestPlanV1_0Test.java b/allure-test-filter/src/test/java/io/qameta/allure/testfilter/TestPlanV1_0Test.java new file mode 100644 index 00000000..b66cf67c --- /dev/null +++ b/allure-test-filter/src/test/java/io/qameta/allure/testfilter/TestPlanV1_0Test.java @@ -0,0 +1,71 @@ +/* + * 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.testfilter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.qameta.allure.Allure; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TestPlanV1_0Test { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @Test + void shouldMatchByAllureIdOrSelector() { + final TestPlanV1_0 plan = new TestPlanV1_0().setTests(List.of( + new TestPlanV1_0.TestCase().setId("A-1"), + new TestPlanV1_0.TestCase().setSelector("pkg.Test#name") + )); + + assertTrue(plan.isSelected("A-1", "other.Test#name")); + assertTrue(plan.isSelected("other-id", "pkg.Test#name")); + assertFalse(plan.isSelected("other-id", "other.Test#name")); + } + + @Test + void shouldDeserializeVersionedPlans() throws Exception { + final TestPlan plan = OBJECT_MAPPER.readValue( + "{" + + "\"version\":\"1.0\"," + + "\"tests\":[{\"id\":\"A-1\",\"selector\":\"pkg.Test#name\"}]" + + "}", + TestPlan.class + ); + + assertInstanceOf(TestPlanV1_0.class, plan); + assertTrue(((TestPlanV1_0) plan).isSelected("A-1", "pkg.Test#name")); + } + + @Test + void shouldFallbackToUnknownPlanForUnknownVersion() throws Exception { + final TestPlan plan = Allure.step("Deserialize a plan with an unsupported version", () -> + OBJECT_MAPPER.readValue( + "{\"version\":\"2.0\"}", + TestPlan.class + ) + ); + + Allure.step("Verify the parser falls back to the unknown plan representation", () -> + assertInstanceOf(TestPlanUnknown.class, plan) + ); + } +} diff --git a/allure-test-filter/src/test/resources/allure.properties b/allure-test-filter/src/test/resources/allure.properties new file mode 100644 index 00000000..08b61b47 --- /dev/null +++ b/allure-test-filter/src/test/resources/allure.properties @@ -0,0 +1,3 @@ +allure.results.directory=build/allure-results +allure.label.epic=#project.description# +allure.label.module=allure-test-filter diff --git a/allure-testng/build.gradle.kts b/allure-testng/build.gradle.kts index 7d6fe56a..7113509d 100644 --- a/allure-testng/build.gradle.kts +++ b/allure-testng/build.gradle.kts @@ -10,10 +10,14 @@ dependencies { testAnnotationProcessor(project(":allure-descriptions-javadoc")) testImplementation("com.google.inject:guice") testImplementation("org.assertj:assertj-core") + testImplementation("org.junit.jupiter:junit-jupiter-api") + testImplementation("org.junit.jupiter:junit-jupiter-params") + testImplementation(project(":allure-jupiter")) testImplementation("org.mockito:mockito-core") testImplementation("org.slf4j:slf4j-simple") testImplementation("org.testng:testng:$testNgVersion") testImplementation(project(":allure-java-commons-test")) + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") } tasks.jar { @@ -28,9 +32,7 @@ tasks.jar { } tasks.test { - useTestNG(closureOf { - suites("src/test/resources/testng.xml") - }) + useJUnitPlatform() exclude("**/samples/*") } diff --git a/allure-testng/src/test/java/io/qameta/allure/testng/AllureTestNgTest.java b/allure-testng/src/test/java/io/qameta/allure/testng/AllureTestNgTest.java index bc1f54bf..498befd5 100644 --- a/allure-testng/src/test/java/io/qameta/allure/testng/AllureTestNgTest.java +++ b/allure-testng/src/test/java/io/qameta/allure/testng/AllureTestNgTest.java @@ -39,9 +39,12 @@ import io.qameta.allure.testng.samples.TestsWithIdForFilter; import org.assertj.core.api.Condition; import org.assertj.core.groups.Tuple; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.testng.TestNG; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; import org.testng.xml.XmlSuite; import java.net.URL; @@ -54,6 +57,7 @@ import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; import static io.qameta.allure.util.ResultsUtils.ALLURE_SEPARATE_LINES_SYSPROP; import static java.lang.String.format; @@ -61,6 +65,7 @@ import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; +import static org.junit.jupiter.params.provider.Arguments.arguments; /** * @author Egor Borisov ehborisov@gmail.com @@ -77,30 +82,30 @@ public class AllureTestNgTest { items.stream().allMatch(item -> item.getSteps().size() == 1), "All items should have a step attached"); - @DataProvider(name = "parallelConfiguration") - public static Object[][] parallelConfiguration() { - return new Object[][]{ - new Object[]{XmlSuite.ParallelMode.NONE, 10}, - new Object[]{XmlSuite.ParallelMode.NONE, 5}, - new Object[]{XmlSuite.ParallelMode.NONE, 2}, - new Object[]{XmlSuite.ParallelMode.NONE, 1}, - new Object[]{XmlSuite.ParallelMode.METHODS, 10}, - new Object[]{XmlSuite.ParallelMode.METHODS, 5}, - new Object[]{XmlSuite.ParallelMode.METHODS, 2}, - new Object[]{XmlSuite.ParallelMode.METHODS, 1}, - new Object[]{XmlSuite.ParallelMode.CLASSES, 10}, - new Object[]{XmlSuite.ParallelMode.CLASSES, 5}, - new Object[]{XmlSuite.ParallelMode.CLASSES, 2}, - new Object[]{XmlSuite.ParallelMode.CLASSES, 1}, - new Object[]{XmlSuite.ParallelMode.INSTANCES, 10}, - new Object[]{XmlSuite.ParallelMode.INSTANCES, 5}, - new Object[]{XmlSuite.ParallelMode.INSTANCES, 2}, - new Object[]{XmlSuite.ParallelMode.INSTANCES, 1}, - new Object[]{XmlSuite.ParallelMode.TESTS, 10}, - new Object[]{XmlSuite.ParallelMode.TESTS, 5}, - new Object[]{XmlSuite.ParallelMode.TESTS, 2}, - new Object[]{XmlSuite.ParallelMode.TESTS, 1}, - }; + @SuppressWarnings("unused") + private static Stream parallelConfiguration() { + return Stream.of( + arguments(XmlSuite.ParallelMode.NONE, 10), + arguments(XmlSuite.ParallelMode.NONE, 5), + arguments(XmlSuite.ParallelMode.NONE, 2), + arguments(XmlSuite.ParallelMode.NONE, 1), + arguments(XmlSuite.ParallelMode.METHODS, 10), + arguments(XmlSuite.ParallelMode.METHODS, 5), + arguments(XmlSuite.ParallelMode.METHODS, 2), + arguments(XmlSuite.ParallelMode.METHODS, 1), + arguments(XmlSuite.ParallelMode.CLASSES, 10), + arguments(XmlSuite.ParallelMode.CLASSES, 5), + arguments(XmlSuite.ParallelMode.CLASSES, 2), + arguments(XmlSuite.ParallelMode.CLASSES, 1), + arguments(XmlSuite.ParallelMode.INSTANCES, 10), + arguments(XmlSuite.ParallelMode.INSTANCES, 5), + arguments(XmlSuite.ParallelMode.INSTANCES, 2), + arguments(XmlSuite.ParallelMode.INSTANCES, 1), + arguments(XmlSuite.ParallelMode.TESTS, 10), + arguments(XmlSuite.ParallelMode.TESTS, 5), + arguments(XmlSuite.ParallelMode.TESTS, 2), + arguments(XmlSuite.ParallelMode.TESTS, 1) + ); } @AllureFeatures.Fixtures @@ -133,7 +138,8 @@ public void shouldSetConfigurationProperty() { } @AllureFeatures.Parallel - @Test(description = "Parallel data provider tests") + @Test + @DisplayName("Parallel data provider tests") public void parallelDataProvider() { final AllureResults results = runTestNgSuites("suites/parallel-data-provider.xml"); List testResult = results.getTestResults(); @@ -143,7 +149,8 @@ public void parallelDataProvider() { } @AllureFeatures.Base - @Test(description = "Singe testng") + @Test + @DisplayName("Singe testng") public void singleTest() { final String testName = "testWithOneStep"; final AllureResults results = runTestNgSuites("suites/single-test.xml"); @@ -162,7 +169,9 @@ public void singleTest() { } @AllureFeatures.Base - @Test(description = "Test with timeout", dataProvider = "parallelConfiguration") + @ParameterizedTest + @MethodSource("parallelConfiguration") + @DisplayName("Test with timeout") public void testWithTimeout(final XmlSuite.ParallelMode mode, final int threadCount) { final String testNameWithTimeout = "testWithTimeout"; @@ -194,7 +203,8 @@ public void testWithTimeout(final XmlSuite.ParallelMode mode, final int threadCo } @AllureFeatures.Descriptions - @Test(description = "Javadoc description with line separation") + @Test + @DisplayName("Javadoc description with line separation") public void descriptionsWithLineSeparationTest() { String initialSeparateLines = System.getProperty(ALLURE_SEPARATE_LINES_SYSPROP); if (!Boolean.parseBoolean(initialSeparateLines)) { @@ -217,7 +227,8 @@ public void descriptionsWithLineSeparationTest() { } @AllureFeatures.Descriptions - @Test(description = "Javadoc description of tests") + @Test + @DisplayName("Javadoc description of tests") public void descriptionsTest() { final String testDescription = "Sample test description"; final AllureResults results = runTestNgSuites("suites/descriptions-test.xml"); @@ -232,7 +243,9 @@ public void descriptionsTest() { } @AllureFeatures.Descriptions - @Test(description = "Javadoc description of befores", dataProvider = "parallelConfiguration") + @ParameterizedTest + @MethodSource("parallelConfiguration") + @DisplayName("Javadoc description of befores") public void descriptionsBefores(final XmlSuite.ParallelMode mode, final int threadCount) { final String beforeClassDescription = "Before class description"; final String beforeMethodDescription = "Before method description"; @@ -257,7 +270,8 @@ public void descriptionsBefores(final XmlSuite.ParallelMode mode, final int thre } @AllureFeatures.Descriptions - @Test(description = "Javadoc description of befores with the same names") + @Test + @DisplayName("Javadoc description of befores with the same names") public void javadocDescriptionsOfBeforesWithTheSameNames() { final AllureResults results = runTestNgSuites("suites/descriptions-test-two-classes.xml"); List testContainers = results.getTestResultContainers(); @@ -270,7 +284,8 @@ public void javadocDescriptionsOfBeforesWithTheSameNames() { } @AllureFeatures.Descriptions - @Test(description = "Javadoc description of tests with the same names") + @Test + @DisplayName("Javadoc description of tests with the same names") public void javadocDescriptionsOfTestsWithTheSameNames() { final AllureResults results = runTestNgSuites("suites/descriptions-test-two-classes.xml"); List testResults = results.getTestResults(); @@ -281,7 +296,8 @@ public void javadocDescriptionsOfTestsWithTheSameNames() { } @AllureFeatures.FailedTests - @Test(description = "Test failing by assertion") + @Test + @DisplayName("Test failing by assertion") public void failingByAssertion() { String testName = "failingByAssertion"; final AllureResults results = runTestNgSuites("suites/failing-by-assertion.xml"); @@ -300,7 +316,8 @@ public void failingByAssertion() { } @AllureFeatures.BrokenTests - @Test(description = "Broken testng") + @Test + @DisplayName("Broken testng") public void brokenTest() { String testName = "brokenTest"; final AllureResults results = runTestNgSuites("suites/broken.xml"); @@ -322,7 +339,8 @@ public void brokenTest() { } @AllureFeatures.BrokenTests - @Test(description = "Broken testng - Exception without message") + @Test + @DisplayName("Broken testng - Exception without message") public void brokenTestWithOutMessage() { String testName = "brokenTestWithoutMessage"; final AllureResults results = runTestNgSuites("suites/brokenWithoutMessage.xml"); @@ -345,7 +363,9 @@ public void brokenTestWithOutMessage() { } @AllureFeatures.Fixtures - @Test(description = "Suite fixtures", dataProvider = "parallelConfiguration") + @ParameterizedTest + @MethodSource("parallelConfiguration") + @DisplayName("Suite fixtures") public void perSuiteFixtures(final XmlSuite.ParallelMode mode, final int threadCount) { String suiteName = "Test suite 12"; String testTagName = "Test tag 12"; @@ -372,7 +392,9 @@ public void perSuiteFixtures(final XmlSuite.ParallelMode mode, final int threadC } @AllureFeatures.Fixtures - @Test(description = "Class fixtures", dataProvider = "parallelConfiguration") + @ParameterizedTest + @MethodSource("parallelConfiguration") + @DisplayName("Class fixtures") public void perClassFixtures(final XmlSuite.ParallelMode mode, final int threadCount) { final AllureResults results = runTestNgSuites( parallel(mode, threadCount), @@ -401,7 +423,9 @@ public void perClassFixtures(final XmlSuite.ParallelMode mode, final int threadC } @AllureFeatures.Fixtures - @Test(description = "Method fixtures", dataProvider = "parallelConfiguration") + @ParameterizedTest + @MethodSource("parallelConfiguration") + @DisplayName("Method fixtures") public void perMethodFixtures(final XmlSuite.ParallelMode mode, final int threadCount) { String suiteName = "Test suite 11"; String testTagName = "Test tag 11"; @@ -430,7 +454,9 @@ public void perMethodFixtures(final XmlSuite.ParallelMode mode, final int thread } @AllureFeatures.Fixtures - @Test(description = "Test fixtures", dataProvider = "parallelConfiguration") + @ParameterizedTest + @MethodSource("parallelConfiguration") + @DisplayName("Test fixtures") public void perTestTagFixtures(final XmlSuite.ParallelMode mode, final int threadCount) { String suiteName = "Test suite 13"; String testTagName = "Test tag 13"; @@ -457,7 +483,8 @@ public void perTestTagFixtures(final XmlSuite.ParallelMode mode, final int threa } @AllureFeatures.SkippedTests - @Test(description = "Skipped suite") + @Test + @DisplayName("Skipped suite") public void skippedSuiteTest() { final Condition skipReason = new Condition<>(step -> step.getStatusDetails().getTrace().startsWith("java.lang.RuntimeException: Skip all"), @@ -494,7 +521,8 @@ public void skippedSuiteTest() { } @AllureFeatures.Base - @Test(description = "Multi suites") + @Test + @DisplayName("Multi suites") public void multipleSuites() { String beforeMethodName = "io.qameta.allure.testng.samples.ParameterizedTest.beforeMethod"; String firstSuiteName = "Test suite 6"; @@ -526,7 +554,8 @@ public void multipleSuites() { @SuppressWarnings("unchecked") @AllureFeatures.Parameters - @Test(description = "Before Suite Parameter") + @Test + @DisplayName("Before Suite Parameter") public void testBeforeSuiteParameter() { final AllureResults results = runTestNgSuites("suites/parameterized-suite1.xml", "suites/parameterized-suite2.xml"); List testResults = results.getTestResults(); @@ -543,7 +572,8 @@ public void testBeforeSuiteParameter() { } @AllureFeatures.Parallel - @Test(description = "Parallel methods") + @Test + @DisplayName("Parallel methods") public void parallelMethods() { String before1 = "io.qameta.allure.testng.samples.ParallelMethods.beforeMethod"; String before2 = "io.qameta.allure.testng.samples.ParallelMethods.beforeMethod2"; @@ -566,7 +596,8 @@ public void parallelMethods() { } @AllureFeatures.Steps - @Test(description = "Nested steps") + @Test + @DisplayName("Nested steps") public void nestedSteps() { String beforeMethod = "io.qameta.allure.testng.samples.NestedSteps.beforeMethod"; String nestedStep = "nestedStep"; @@ -599,7 +630,8 @@ public void nestedSteps() { } @AllureFeatures.MarkerAnnotations - @Test(description = "Flaky tests") + @Test + @DisplayName("Flaky tests") public void flakyTests() { final AllureResults results = runTestNgSuites("suites/flaky.xml"); @@ -621,7 +653,8 @@ public void flakyTests() { } @AllureFeatures.MarkerAnnotations - @Test(description = "Muted tests") + @Test + @DisplayName("Muted tests") public void mutedTests() { final AllureResults results = runTestNgSuites("suites/muted.xml"); @@ -643,7 +676,8 @@ public void mutedTests() { } @AllureFeatures.Links - @Test(description = "Tests with links") + @Test + @DisplayName("Tests with links") public void linksTest() { final AllureResults results = runTestNgSuites("suites/links.xml"); @@ -663,7 +697,8 @@ public void linksTest() { } @AllureFeatures.MarkerAnnotations - @Test(description = "BDD annotations") + @Test + @DisplayName("BDD annotations") public void bddAnnotationsTest() { final AllureResults results = runTestNgSuites("suites/bdd-annotations.xml"); @@ -689,7 +724,8 @@ public void bddAnnotationsTest() { } @AllureFeatures.Base - @Test(description = "Should support TestNG retries") + @Test + @DisplayName("Should support TestNG retries") public void retryTest() { final AllureResults results = runTestNgSuites("suites/retry.xml"); List testResults = results.getTestResults(); @@ -698,7 +734,8 @@ public void retryTest() { } @AllureFeatures.Severity - @Test(description = "Should add severity for tests") + @Test + @DisplayName("Should add severity for tests") public void severityTest() { final AllureResults results = runTestNgSuites("suites/severity.xml"); List testResults = results.getTestResults(); @@ -711,7 +748,8 @@ public void severityTest() { } @AllureFeatures.MarkerAnnotations - @Test(description = "Should add owner to tests") + @Test + @DisplayName("Should add owner to tests") public void ownerTest() { final AllureResults results = runTestNgSuites("suites/owner.xml"); List testResults = results.getTestResults(); @@ -736,7 +774,8 @@ public void ownerTest() { } @AllureFeatures.MarkerAnnotations - @Test(description = "Should add tag to tests") + @Test + @DisplayName("Should add tag to tests") public void tagTest() { final AllureResults results = runTestNgSuites("suites/tags.xml"); List testResults = results.getTestResults(); @@ -759,7 +798,8 @@ public void tagTest() { } @AllureFeatures.Attachments - @Test(description = "Should add attachments to tests") + @Test + @DisplayName("Should add attachments to tests") public void attachmentsTest() { final AllureResults results = runTestNgSuites("suites/attachments.xml"); List testResults = results.getTestResults(); @@ -773,7 +813,8 @@ public void attachmentsTest() { @AllureFeatures.MarkerAnnotations @Issue("42") - @Test(description = "Should process flaky for failed tests") + @Test + @DisplayName("Should process flaky for failed tests") public void shouldAddFlakyToFailedTests() { final AllureResults results = runTestNgSuites("suites/gh-42.xml"); @@ -789,7 +830,8 @@ public void shouldAddFlakyToFailedTests() { } @AllureFeatures.History - @Test(description = "Should use parameters for history id") + @Test + @DisplayName("Should use parameters for history id") public void shouldUseParametersForHistoryIdGeneration() { final AllureResults results = runTestNgSuites("suites/history-id-parameters.xml"); @@ -800,7 +842,8 @@ public void shouldUseParametersForHistoryIdGeneration() { } @AllureFeatures.History - @Test(description = "Should generate the same history id for the same tests") + @Test + @DisplayName("Should generate the same history id for the same tests") public void shouldGenerateSameHistoryIdForTheSameTests() { final AllureResults results = runTestNgSuites("suites/history-id-the-same.xml"); @@ -814,7 +857,8 @@ public void shouldGenerateSameHistoryIdForTheSameTests() { @SuppressWarnings("unchecked") @AllureFeatures.Fixtures @Issue("67") - @Test(description = "Should set correct status for fixtures") + @Test + @DisplayName("Should set correct status for fixtures") public void shouldSetCorrectStatusesForFixtures() { final AllureResults results = runTestNgSuites( "suites/per-suite-fixtures-combination.xml", @@ -860,7 +904,8 @@ public void shouldSetCorrectStatusesForFixtures() { @SuppressWarnings("unchecked") @AllureFeatures.Fixtures @Issue("67") - @Test(description = "Should set correct status for failed before fixtures") + @Test + @DisplayName("Should set correct status for failed before fixtures") public void shouldSetCorrectStatusForFailedBeforeFixtures() { final AllureResults results = runTestNgSuites( "suites/failed-before-suite-fixture.xml", @@ -882,7 +927,8 @@ public void shouldSetCorrectStatusForFailedBeforeFixtures() { @SuppressWarnings("unchecked") @AllureFeatures.Fixtures @Issue("67") - @Test(description = "Should set correct status for failed after fixtures") + @Test + @DisplayName("Should set correct status for failed after fixtures") public void shouldSetCorrectStatusForFailedAfterFixtures() { final Consumer configurer = parallel(XmlSuite.ParallelMode.METHODS, 5); @@ -906,7 +952,8 @@ public void shouldSetCorrectStatusForFailedAfterFixtures() { @AllureFeatures.Parameters @Issue("97") - @Test(description = "Should process varargs test parameters") + @Test + @DisplayName("Should process varargs test parameters") public void shouldProcessVarargsParameters() { final AllureResults results = runTestNgSuites("suites/gh-97.xml"); @@ -921,7 +968,8 @@ public void shouldProcessVarargsParameters() { @AllureFeatures.Fixtures @Issue("99") - @Test(description = "Should attach class fixtures correctly") + @Test + @DisplayName("Should attach class fixtures correctly") public void shouldAttachClassFixturesCorrectly() { final Consumer configurer = parallel(XmlSuite.ParallelMode.METHODS, 5); @@ -974,7 +1022,8 @@ public void shouldAttachClassFixturesCorrectly() { @AllureFeatures.History @Issue("102") - @Test(description = "Should generate different history id for inherited tests") + @Test + @DisplayName("Should generate different history id for inherited tests") public void shouldGenerateDifferentHistoryIdForInheritedTests() { final AllureResults results = runTestNgSuites("suites/gh-102.xml"); @@ -985,7 +1034,8 @@ public void shouldGenerateDifferentHistoryIdForInheritedTests() { @AllureFeatures.Fixtures @Issue("101") - @Test(description = "Should use fixture description") + @Test + @DisplayName("Should use fixture description") public void shouldUseFixtureDescriptions() { final AllureResults results = runTestNgSuites("suites/gh-101.xml"); @@ -1009,10 +1059,9 @@ public void shouldProcessCyrillicDescriptions() { @AllureFeatures.Fixtures @AllureFeatures.Parallel @Issue("219") - @Test( - description = "Should not mix up fixtures during parallel run", - dataProvider = "parallelConfiguration" - ) + @ParameterizedTest + @MethodSource("parallelConfiguration") + @DisplayName("Should not mix up fixtures during parallel run") public void shouldAddCorrectBeforeMethodFixturesInCaseOfParallelRun( final XmlSuite.ParallelMode mode, final int threadCount) { final AllureResults results = runTestNgSuites( @@ -1149,7 +1198,8 @@ public void shouldProcessArrayParameters() { @SuppressWarnings("unchecked") @AllureFeatures.Fixtures @Issue("304") - @Test(dataProvider = "parallelConfiguration") + @ParameterizedTest + @MethodSource("parallelConfiguration") public void shouldProcessFailedSetUps(final XmlSuite.ParallelMode mode, final int threadCount) { final AllureResults results = runTestNgSuites(parallel(mode, threadCount), "suites/gh-304.xml"); @@ -1204,16 +1254,17 @@ public void shouldSupportFactoryOnConstructor() { ); } - @DataProvider(name = "failedFixtures") - public Object[][] failedFixtures() { - return new Object[][]{ - {"suites/failed-before-test-fixture.xml", "beforeTest"}, - {"suites/failed-before-class-fixture.xml", "beforeClass"}, - {"suites/failed-before-suite-fixture.xml", "beforeSuite"} - }; + @SuppressWarnings("unused") + private static Stream failedFixtures() { + return Stream.of( + arguments("suites/failed-before-test-fixture.xml", "beforeTest"), + arguments("suites/failed-before-class-fixture.xml", "beforeClass"), + arguments("suites/failed-before-suite-fixture.xml", "beforeSuite") + ); } - @Test(dataProvider = "failedFixtures") + @ParameterizedTest + @MethodSource("failedFixtures") @AllureFeatures.Fixtures public void shouldAddBeforeFixtureToFakeTestResult(final String suite, final String fixture) { final AllureResults results = runTestNgSuites(suite); @@ -1522,7 +1573,8 @@ public AllureResults runTestPlan(final TestPlan plan, final Class... testClas } @AllureFeatures.Fixtures - @Test(description = "Should process data provider in setup") + @Test + @DisplayName("Should process data provider in setup") public void shouldProcessDataProviderInSetup() { final AllureResults results = runTestNgSuites("suites/data-provider-with-attachment.xml"); @@ -1541,7 +1593,8 @@ public void shouldProcessDataProviderInSetup() { } @AllureFeatures.Fixtures - @Test(description = "Should process failed data provider in setup") + @Test + @DisplayName("Should process failed data provider in setup") public void shouldProcessFailedDataProviderInSetup() { final AllureResults results = runTestNgSuites("suites/failed-data-provider.xml"); @@ -1552,7 +1605,8 @@ public void shouldProcessFailedDataProviderInSetup() { } @AllureFeatures.Fixtures - @Test(description = "Should process flaky data provider in setup") + @Test + @DisplayName("Should process flaky data provider in setup") public void shouldProcessFlakyDataProvider() { final AllureResults results = runTestNgSuites("suites/flaky-data-provider.xml"); @@ -1566,7 +1620,8 @@ public void shouldProcessFlakyDataProvider() { } @AllureFeatures.Fixtures - @Test(description = "Should properly link data provider container to test result") + @Test + @DisplayName("Should properly link data provider container to test result") public void shouldProperlyLinkDataProviderContainerToTestResult() { final AllureResults results = runTestNgSuites("suites/data-provider-with-attachment.xml"); @@ -1581,7 +1636,8 @@ public void shouldProperlyLinkDataProviderContainerToTestResult() { } @AllureFeatures.Fixtures - @Test(description = "Should link multiple tests to data provider container") + @Test + @DisplayName("Should link multiple tests to data provider container") public void shouldLinkMultipleTestsToDataProviderContainer() { final AllureResults results = runTestNgSuites("suites/data-provider-multiple-tests.xml"); @@ -1605,7 +1661,8 @@ public void shouldLinkMultipleTestsToDataProviderContainer() { } @AllureFeatures.Fixtures - @Test(description = "Should link inherited data provider") + @Test + @DisplayName("Should link inherited data provider") public void shouldLinkInheritedDataProvider() { final AllureResults results = runTestNgSuites("suites/data-provider-inheritance.xml"); @@ -1620,7 +1677,8 @@ public void shouldLinkInheritedDataProvider() { } @AllureFeatures.Fixtures - @Test(description = "Should link correct data provider in multiple classes") + @Test + @DisplayName("Should link correct data provider in multiple classes") public void shouldLinkCorrectDataProviderInMultipleClasses() { final AllureResults results = runTestNgSuites("suites/data-provider-multiple-classes.xml"); @@ -1639,7 +1697,8 @@ public void shouldLinkCorrectDataProviderInMultipleClasses() { } @AllureFeatures.Fixtures - @Test(description = "Should process parallel data provider") + @Test + @DisplayName("Should process parallel data provider") public void shouldProcessParallelDataProvider() { final AllureResults results = runTestNgSuites("suites/data-provider-parallel.xml"); diff --git a/allure-testng/src/test/resources/allure.properties b/allure-testng/src/test/resources/allure.properties index e4f1d9fc..69cd85a7 100644 --- a/allure-testng/src/test/resources/allure.properties +++ b/allure-testng/src/test/resources/allure.properties @@ -1,3 +1,4 @@ allure.results.directory=build/allure-results allure.link.issue.pattern=https://github.com/allure-framework/allure-java/issues/{} allure.label.epic=#project.description# +allure.label.module=allure-testng diff --git a/allurerc.mjs b/allurerc.mjs new file mode 100644 index 00000000..0e718dee --- /dev/null +++ b/allurerc.mjs @@ -0,0 +1,22 @@ +const { ALLURE_SERVICE_TOKEN } = process.env; + +const allureService = ALLURE_SERVICE_TOKEN + ? { + accessToken: ALLURE_SERVICE_TOKEN, + legacy: true, + } + : undefined; + +export default { + name: "Allure Java", + output: "./build/allure-report", + plugins: { + awesome: { + options: { + groupBy: ["module", "parentSuite", "suite", "subSuite"], + publish: true, + }, + }, + }, + ...(allureService ? { allureService } : {}), +}; diff --git a/build.gradle.kts b/build.gradle.kts index ac086dec..96291357 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ val libs = subprojects.filterNot { it.name in "allure-bom" } val standardJavaLibs = libs.filterNot { it.name == "allure-scalatest" } tasks.withType(Wrapper::class) { - gradleVersion = "8.5" + gradleVersion = "8.11" } plugins { @@ -21,8 +21,7 @@ plugins { id("com.github.spotbugs") id("com.diffplug.spotless") id("io.github.gradle-nexus.publish-plugin") - id("io.qameta.allure-adapter") apply false - id("io.qameta.allure-report") + id("io.qameta.allure") id("io.spring.dependency-management") } @@ -138,8 +137,7 @@ configure(libs) { apply(plugin = "pmd") apply(plugin = "com.github.spotbugs") apply(plugin = "com.diffplug.spotless") - apply(plugin = "io.qameta.allure-report") - apply(plugin = "io.qameta.allure-adapter") + apply(plugin = "io.qameta.allure") apply(plugin = "io.spring.dependency-management") apply(plugin = "java") apply(plugin = "java-library") @@ -258,15 +256,20 @@ configure(libs) { allure { adapter { + // Many modules carry third-party test frameworks on the classpath as integration fixtures, + // so never let the Gradle plugin auto-detect adapters from dependencies alone. autoconfigure.set(false) aspectjWeaver.set(true) aspectjVersion.set(dependencyManagement.managedVersions["org.aspectj:aspectjweaver"]) - // in order to disable dependencySubstitution (spi-off classifier) - autoconfigureListeners.set(true) - - afterEvaluate { - frameworks.forEach { adapter -> adapter.enabled.set(false) } + // Every Gradle test task in this build runs on JUnit Platform now. + // Avoid mentioning unused adapters here because allure-gradle adds mentioned + // adapters to the test classpath even when we do not execute that framework. + frameworks { + junitPlatform { + enabled.set(true) + autoconfigureListeners.set(true) + } } } } diff --git a/docs/allure-agent-mode.md b/docs/allure-agent-mode.md new file mode 100644 index 00000000..f9948201 --- /dev/null +++ b/docs/allure-agent-mode.md @@ -0,0 +1,161 @@ +# Allure Agent Mode + +Use Allure agent-mode to design, review, validate, debug, and enrich tests in this repository. + +## Project Context + +- This repository is a Gradle multi-project build. Use `./gradlew` for repo-local test commands. +- Prefer the narrowest relevant scope first, usually a module task such as `:allure-jupiter:test` or a single test via `--tests`. +- CI's broad verification entry point is `./gradlew --no-build-cache cleanTest test`. +- Many modules already emit framework results to `/build/allure-results`; agent mode adds a separate per-run review artifact layer and does not replace those module outputs. +- If `allure run` is unavailable in the local agent environment, fix that first before treating console-only runs as authoritative. + +## Review Principle + +Runtime first, source second. + +- If a command executes tests and its result will be used for smoke checking, reasoning, review, coverage analysis, debugging, or any user-facing conclusion, run it through `allure run`. It preserves the original console logs and adds agent-mode artifacts when you need them. +- If the agent-mode output is missing or incomplete, debug that first and treat console-only conclusions as provisional. + +## Verification Standard + +- Use `allure run` for smoke checks too, even when the change is small or mechanical. +- Only skip agent mode when it is impossible or when you are debugging agent mode itself. + +## Core Loops + +### Test Review Loop + +1. Identify the exact review scope. +2. Create a fresh expectations file for this run in a temp directory. +3. Run only that scope with `allure run`. +4. Read `index.md`, `manifest/run.json`, `manifest/tests.jsonl`, and `manifest/findings.jsonl`. +5. Read per-test markdown only for tests that failed, drifted, or have findings. +6. Only after runtime review, inspect source code for root cause or coverage gaps. +7. If evidence is weak or partial, enrich the tests and rerun. + +### Feature Delivery Loop + +1. Understand the feature or issue. +2. Create a fresh expectations file for this run in a temp directory. +3. Write or update the tests. +4. Run the target Gradle scope with `allure run`. +5. Review `index.md`, manifests, and per-test markdown. +6. Enrich tests when evidence is weak. +7. Rerun until scope and evidence are acceptable. + +### Metadata Enrichment Loop + +Use this when the run is functionally correct but too weak to review: + +1. Identify missing or low-signal findings. +2. Add real steps, attachments, or minimal metadata. +3. Rerun the same intended scope. +4. Reject noop-style or placeholder evidence. + +### Small Test Change Workflow + +1. Create a fresh expectations file and temp output directory for the touched scope. +2. Run the touched scope with `allure run`, even if the goal is only a smoke check after a mechanical change such as typing cleanup, mock refactors, or helper extraction. +3. Review `index.md`, `manifest/run.json`, `manifest/tests.jsonl`, and `manifest/findings.jsonl`. +4. Only then make a final statement about regression safety or test correctness. + +### Coverage Review Workflow + +1. Split command, package, or module audits into scoped groups. +2. Give each group its own expectations file and temp output directory. +3. Run each group with `allure run`. +4. Review runtime artifacts first, then inspect source code only after the run explains what actually executed. +5. Mark the review incomplete until each scoped group either matched expectations or was explicitly documented as a broad package-health audit. + +## Per-Run Artifacts + +- `ALLURE_AGENT_OUTPUT` must use a unique temp directory per run. +- `ALLURE_AGENT_EXPECTATIONS` must use a unique temp file per run. +- Do not reuse those paths across parallel runs. +- Keep agent-mode artifacts in temp locations, not in committed repo paths or module `build/allure-results` directories. + +YAML is preferred for expectations in v1. + +Review-oriented expectations example: + +```yaml +goal: Review a module-scoped Gradle test run +task_id: module-review +notes: + - Start with the smallest relevant Gradle test scope. + - Review runtime evidence before source inspection. +``` + +Targeted module-run pattern: + +```bash +TMP_DIR="$(mktemp -d)" +EXPECTATIONS="$TMP_DIR/expectations.yaml" + +cat > "$EXPECTATIONS" <<'YAML' +goal: Review a module-scoped Gradle test run +task_id: module-review +notes: + - Start with the smallest relevant Gradle test scope. + - Review runtime evidence before source inspection. +YAML + +ALLURE_AGENT_OUTPUT="$TMP_DIR/agent-output" \ +ALLURE_AGENT_EXPECTATIONS="$EXPECTATIONS" \ +allure run -- ./gradlew :allure-jupiter:test \ + --tests io.qameta.allure.junit5.AllureJunit5Junit6CompatibilityTest +``` + +Broad repo-smoke pattern: + +```bash +TMP_DIR="$(mktemp -d)" + +ALLURE_AGENT_OUTPUT="$TMP_DIR/agent-output" \ +allure run -- ./gradlew --no-build-cache cleanTest test +``` + +Broad package-health or repo-health audits may omit expectations, but the resulting scope review is weaker and should be called out explicitly. + +## Evidence Rules + +- Steps must wrap real setup, actions, state transitions, or assertions. +- Attachments must contain real runtime evidence from that execution. +- Metadata should stay minimal and purposeful. +- Prefer helper-boundary instrumentation over repetitive caller wrapping. + +Good example: + +- instrument a shared assertion helper once instead of wrapping every caller + +Rejected examples: + +- empty wrapper steps +- static `test passed` attachments +- labels that no review or policy step uses + +## When Console Errors Are Not Represented As Test Results + +- Suite-load, import, or setup failures may appear only in `artifacts/global/stderr.txt` or global errors. +- If `manifest/tests.jsonl` does not account for all visible failures from the test runner, inspect global stderr before concluding the run is fully modeled. +- Treat that state as a partial runtime review, not as a clean or complete result set. +- If runner-visible failures are present outside logical test files, final conclusions must stay provisional until the missing modeling is understood. + +## Acceptance Rules + +Accept a run only when: + +- scope matches expectations +- evidence is strong enough to explain what happened +- no high-confidence noop or placeholder findings remain + +### Review Completeness + +A test review is not complete unless: + +- the relevant scope was run with agent mode, unless that is impossible +- expectations were created for the intended scope, unless this is a broad package-health audit +- agent artifacts were reviewed before final conclusions +- missing or partial runtime modeling was called out explicitly +- console-only conclusions are treated as provisional when agent output is absent or incomplete diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a5952066..e48eca57 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index ad6f5e51..02cf649a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -49,10 +49,7 @@ pluginManagement { id("com.diffplug.spotless") version "6.25.0" id("io.github.goooler.shadow") version "8.1.8" id("io.github.gradle-nexus.publish-plugin") version "2.0.0" - id("io.qameta.allure-adapter") version "3.0.1" - id("io.qameta.allure-aggregate-report") version "3.0.1" - id("io.qameta.allure-download") version "3.0.1" - id("io.qameta.allure-report") version "3.0.1" + id("io.qameta.allure") version "4.0.0" id("io.spring.dependency-management") version "1.1.7" id("com.google.protobuf") version "0.9.6" id("com.github.spotbugs") version "6.4.7" From d1da0de8882710fa62f55ecad3334228c8dc5c25 Mon Sep 17 00:00:00 2001 From: Dmitry Baev Date: Fri, 8 May 2026 09:21:11 +0100 Subject: [PATCH 06/13] improve assertj reporting with readable assertion chains (via #1268) --- .../qameta/allure/assertj/AllureAspectJ.java | 150 +++-- .../qameta/allure/assertj/AssertJChain.java | 123 ++++ .../assertj/AssertJLifecycleListener.java | 44 ++ .../allure/assertj/AssertJMethodSupport.java | 96 +++ .../allure/assertj/AssertJOperation.java | 160 +++++ .../allure/assertj/AssertJRecorder.java | 261 ++++++++ .../allure/assertj/AssertJValueRenderer.java | 556 ++++++++++++++++++ ...a.allure.listener.FixtureLifecycleListener | 1 + ...meta.allure.listener.TestLifecycleListener | 1 + .../allure/assertj/AllureAspectJTest.java | 452 ++++++++++++-- .../io/qameta/allure/util/ObjectUtils.java | 4 +- .../io/qameta/allure/util/ResultsUtils.java | 47 ++ .../io/qameta/allure/ResultsUtilsTest.java | 41 ++ .../qameta/allure/util/ObjectUtilsTest.java | 14 + 14 files changed, 1849 insertions(+), 101 deletions(-) create mode 100644 allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJChain.java create mode 100644 allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJLifecycleListener.java create mode 100644 allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJMethodSupport.java create mode 100644 allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJOperation.java create mode 100644 allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJRecorder.java create mode 100644 allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJValueRenderer.java create mode 100644 allure-assertj/src/main/resources/META-INF/services/io.qameta.allure.listener.FixtureLifecycleListener create mode 100644 allure-assertj/src/main/resources/META-INF/services/io.qameta.allure.listener.TestLifecycleListener diff --git a/allure-assertj/src/main/java/io/qameta/allure/assertj/AllureAspectJ.java b/allure-assertj/src/main/java/io/qameta/allure/assertj/AllureAspectJ.java index dbd92dc3..00af9c09 100644 --- a/allure-assertj/src/main/java/io/qameta/allure/assertj/AllureAspectJ.java +++ b/allure-assertj/src/main/java/io/qameta/allure/assertj/AllureAspectJ.java @@ -17,28 +17,22 @@ import io.qameta.allure.Allure; import io.qameta.allure.AllureLifecycle; -import io.qameta.allure.model.Status; -import io.qameta.allure.model.StepResult; -import io.qameta.allure.util.ObjectUtils; +import org.assertj.core.api.AbstractAssert; import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.AfterReturning; -import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import java.util.UUID; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static io.qameta.allure.util.ResultsUtils.getStatus; -import static io.qameta.allure.util.ResultsUtils.getStatusDetails; +import java.util.function.Supplier; /** + * Captures user-side AssertJ factories and fluent calls, then delegates assertion-chain state + * to {@link AssertJRecorder}. + * * @author charlie (Dmitry Baev). * @author sskorol (Sergey Korol). */ @@ -46,8 +40,6 @@ @Aspect public class AllureAspectJ { - private static final Logger LOGGER = LoggerFactory.getLogger(AllureAspectJ.class); - private static InheritableThreadLocal lifecycle = new InheritableThreadLocal() { @Override protected AllureLifecycle initialValue() { @@ -55,64 +47,73 @@ protected AllureLifecycle initialValue() { } }; - @Pointcut("execution(!private org.assertj.core.api.AbstractAssert.new(..))") - public void anyAssertCreation() { + private static final ThreadLocal RECORDER = ThreadLocal.withInitial(AssertJRecorder::new); + + private static final ThreadLocal RECORDING_MUTED = ThreadLocal.withInitial(() -> false); + + @Pointcut("(" + + "call(public static * org.assertj.core.api.Assertions*.assertThat*(..))" + + " || call(public static * org.assertj.core.api.BDDAssertions*.then*(..))" + + " || call(public * org.assertj.core.api.*SoftAssertionsProvider+.assertThat*(..))" + + " || call(public * org.assertj.core.api.*SoftAssertionsProvider+.then*(..))" + + ")") + public void assertFactoryCall() { //pointcut body, should be empty } - @Pointcut("execution(* org.assertj.core.api.AssertJProxySetup.*(..))") - public void proxyMethod() { + @Pointcut("(" + + "call(public * org.assertj.core.api.AbstractAssert+.*(..))" + + " || call(public * org.assertj.core.api.Assert+.*(..))" + + " || call(public * org.assertj.core.api.Descriptable+.*(..))" + + ")" + + " && target(assertion)") + public void assertOperationCall(final AbstractAssert assertion) { //pointcut body, should be empty } - @Pointcut("execution(public * org.assertj.core.api.AbstractAssert+.*(..)) && !proxyMethod()") - public void anyAssert() { + @Pointcut("!within(org.assertj..*) && !within(io.qameta.allure.assertj.AllureAspectJ)") + public void userCodeCall() { //pointcut body, should be empty } - @After("anyAssertCreation()") - public void logAssertCreation(final JoinPoint joinPoint) { - final String actual = joinPoint.getArgs().length > 0 - ? ObjectUtils.toString(joinPoint.getArgs()[0]) - : ""; - final String uuid = UUID.randomUUID().toString(); - final String name = String.format("assertThat \'%s\'", actual); - - final StepResult result = new StepResult() - .setName(name) - .setStatus(Status.PASSED); + @AfterReturning(pointcut = "assertFactoryCall() && userCodeCall()", returning = "result") + public void logAssertCreation(final JoinPoint joinPoint, final Object result) { + if (isRecordingMuted() || !(result instanceof AbstractAssert)) { + return; + } - getLifecycle().startStep(uuid, result); - getLifecycle().stopStep(uuid); + final AbstractAssert assertion = (AbstractAssert) result; + getRecorder().assertionCreated(getLifecycle(), assertion, firstArgumentOf(joinPoint)); } - @Before("anyAssert()") - public void stepStart(final JoinPoint joinPoint) { - final MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); - - final String uuid = UUID.randomUUID().toString(); - final String name = joinPoint.getArgs().length > 0 - ? String.format("%s \'%s\'", methodSignature.getName(), arrayToString(joinPoint.getArgs())) - : methodSignature.getName(); - - final StepResult result = new StepResult() - .setName(name); - - getLifecycle().startStep(uuid, result); - } + @Around("assertOperationCall(assertion) && userCodeCall()") + public Object logAssertOperation(final ProceedingJoinPoint joinPoint, + final AbstractAssert assertion) throws Throwable { + final String methodName = getMethodName(joinPoint); + if (isRecordingMuted() || getRecorder().isIgnored(methodName)) { + return joinPoint.proceed(); + } - @AfterThrowing(pointcut = "anyAssert()", throwing = "e") - public void stepFailed(final Throwable e) { - getLifecycle().updateStep(s -> s - .setStatus(getStatus(e).orElse(Status.BROKEN)) - .setStatusDetails(getStatusDetails(e).orElse(null))); - getLifecycle().stopStep(); + final AssertJOperation operation = getRecorder().startOperation( + getLifecycle(), + assertion, + methodName, + joinPoint.getArgs() + ); + try { + final Object result = joinPoint.proceed(); + getRecorder().operationPassed(operation, result); + return result; + } catch (Throwable throwable) { + getRecorder().operationFailed(operation, throwable); + throw throwable; + } } - @AfterReturning(pointcut = "anyAssert()") - public void stepStop() { - getLifecycle().updateStep(s -> s.setStatus(Status.PASSED)); - getLifecycle().stopStep(); + @After("execution(public void org.assertj.core.api.DefaultAssertionErrorCollector.collectAssertionError(" + + "java.lang.AssertionError)) && args(error)") + public void softAssertionFailed(final AssertionError error) { + getRecorder().softAssertionFailed(error); } /** @@ -122,15 +123,40 @@ public void stepStop() { */ public static void setLifecycle(final AllureLifecycle allure) { lifecycle.set(allure); + clearContext(); } public static AllureLifecycle getLifecycle() { return lifecycle.get(); } - private static String arrayToString(final Object... array) { - return Stream.of(array) - .map(ObjectUtils::toString) - .collect(Collectors.joining(" ")); + public static void clearContext() { + RECORDER.remove(); + } + + static T withoutRecording(final Supplier supplier) { + final boolean previous = RECORDING_MUTED.get(); + RECORDING_MUTED.set(true); + try { + return supplier.get(); + } finally { + RECORDING_MUTED.set(previous); + } + } + + private static AssertJRecorder getRecorder() { + return RECORDER.get(); + } + + private static boolean isRecordingMuted() { + return RECORDING_MUTED.get(); + } + + private static Object firstArgumentOf(final JoinPoint joinPoint) { + return joinPoint.getArgs().length == 0 ? null : joinPoint.getArgs()[0]; + } + + private static String getMethodName(final ProceedingJoinPoint joinPoint) { + return ((MethodSignature) joinPoint.getSignature()).getMethod().getName(); } } diff --git a/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJChain.java b/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJChain.java new file mode 100644 index 00000000..c4d30c75 --- /dev/null +++ b/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJChain.java @@ -0,0 +1,123 @@ +/* + * 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.assertj; + +import io.qameta.allure.model.Stage; +import io.qameta.allure.model.Status; +import io.qameta.allure.model.StatusDetails; +import io.qameta.allure.model.StepResult; +import org.assertj.core.api.AbstractAssert; + +import java.util.Optional; +import java.util.UUID; + +/** + * Parent Allure step for one AssertJ assertion chain. + * + *

A chain is the stable container for all meaningful fluent operations produced by one AssertJ assertion object. + * {@link AssertJRecorder} creates it when user code calls an AssertJ factory such as {@code assertThat(actual)}, + * stores it by assertion object identity, and appends one {@link AssertJOperation} child for every reported fluent + * call. Methods such as {@code extracting}, {@code first}, or {@code asInstanceOf} can return another assertion + * object, but they should still read as the same assertion story, so the returned assertion is associated with this + * chain instead of creating an unrelated top-level step.

+ * + *

For a scalar assertion:

+ *
{@code
+ * assertThat("Data").hasSize(4)
+ *
+ * assert "Data"
+ *   has size 4
+ * }
+ * + *

For an assertion with a description, the parent step is renamed while the operation history stays visible:

+ *
{@code
+ * assertThat(user).as("user profile").isNotNull()
+ *
+ * assert user profile
+ *   described as "user profile"
+ *   is not null
+ * }
+ * + *

For navigation or extraction, later checks remain under the same parent:

+ *
{@code
+ * assertThat(results).extracting(Result::getName).containsExactly("passed")
+ *
+ * assert 1 Result item
+ *   extracts Result::getName -> 1 string
+ *   contains exactly ["passed"]
+ * }
+ * + *

This class is intentionally only a small mutable model around the retained {@link StepResult}. It owns the + * parent step name, status, timing, and child operation list. It does not decide which AssertJ methods are meaningful + * or how subjects and arguments are rendered; those decisions belong to {@link AssertJRecorder}, + * {@link AssertJMethodSupport}, and {@link AssertJValueRenderer}.

+ */ +final class AssertJChain { + + private static final String ASSERTJ_STEP_PREFIX = "assert "; + + private final String uuid; + + private final AbstractAssert assertion; + + private final StepResult step; + + AssertJChain(final AbstractAssert assertion, final String subject) { + this.uuid = UUID.randomUUID().toString(); + this.assertion = assertion; + this.step = new StepResult() + .setName(chainName(subject)) + .setStatus(Status.PASSED) + .setStage(Stage.FINISHED) + .setStart(System.currentTimeMillis()) + .setStop(System.currentTimeMillis()); + } + + String getUuid() { + return uuid; + } + + AbstractAssert getAssertion() { + return assertion; + } + + StepResult getStep() { + return step; + } + + void addOperation(final AssertJOperation operation) { + step.getSteps().add(operation.getStep()); + } + + void rename(final Optional description) { + description.ifPresent(value -> step.setName(chainName(value))); + } + + void updateStatus(final Status status, final StatusDetails details) { + step + .setStatus(status) + .setStatusDetails(details); + finish(); + } + + void finish() { + step.setStop(System.currentTimeMillis()); + } + + private String chainName(final String subject) { + return AssertJValueRenderer.truncateStepName(ASSERTJ_STEP_PREFIX + subject); + } +} diff --git a/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJLifecycleListener.java b/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJLifecycleListener.java new file mode 100644 index 00000000..c9ea7a1e --- /dev/null +++ b/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJLifecycleListener.java @@ -0,0 +1,44 @@ +/* + * 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.assertj; + +import io.qameta.allure.listener.FixtureLifecycleListener; +import io.qameta.allure.listener.TestLifecycleListener; +import io.qameta.allure.model.FixtureResult; +import io.qameta.allure.model.TestResult; + +/** + * Clears per-thread AssertJ recorder state after Allure has finished owning the current result. + * + *

{@link AllureAspectJ} keeps an {@link AssertJRecorder} in a {@link ThreadLocal} so assertion objects can + * be matched by identity across later fluent calls. Test engines commonly reuse worker threads, so that + * thread-local map would otherwise keep old assertion objects, rendered steps, and operation stack state after + * the test or fixture result has already been written. The retained {@code StepResult}s are already attached to + * the Allure model by reference, so removing the recorder here does not remove any reported steps; it only + * releases per-thread bookkeeping before the next test or fixture starts on the same thread.

+ */ +public class AssertJLifecycleListener implements TestLifecycleListener, FixtureLifecycleListener { + + @Override + public void afterTestWrite(final TestResult result) { + AllureAspectJ.clearContext(); + } + + @Override + public void afterFixtureStop(final FixtureResult result) { + AllureAspectJ.clearContext(); + } +} diff --git a/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJMethodSupport.java b/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJMethodSupport.java new file mode 100644 index 00000000..685e51bf --- /dev/null +++ b/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJMethodSupport.java @@ -0,0 +1,96 @@ +/* + * 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.assertj; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Keeps method-name decisions out of the aspect and recorder flow. + */ +final class AssertJMethodSupport { + + private static final String AS = "as"; + private static final String DESCRIBED_AS = "describedAs"; + + private static final List IGNORED_METHODS = Arrays.asList( + "actual", + "descriptionText", + "equals", + "getWritableAssertionInfo", + "hashCode", + "toString" + ); + + private static final Set NAVIGATION_METHODS = new HashSet<>(Arrays.asList( + "asBase64Decoded", + "asBoolean", + "asByte", + "asDouble", + "asFloat", + "asInstanceOf", + "asInt", + "asList", + "asLong", + "asShort", + "asString", + "bytes", + "decodedAsBase64", + "element", + "elements", + "extracting", + "extractingResultOf", + "first", + "flatExtracting", + "flatMap", + "last", + "map", + "rootCause", + "singleElement", + "size", + "usingRecursiveAssertion", + "usingRecursiveComparison" + )); + + private AssertJMethodSupport() { + throw new IllegalStateException("do not instantiate"); + } + + static boolean isIgnored(final String methodName) { + return IGNORED_METHODS.contains(methodName); + } + + static String normalize(final String methodName) { + final int accessorIndex = methodName.indexOf("$accessor$"); + if (accessorIndex > 0) { + return methodName.substring(0, accessorIndex); + } + if (DESCRIBED_AS.equals(methodName)) { + return AS; + } + return methodName; + } + + static boolean isDescription(final String methodName) { + return AS.equals(methodName) || DESCRIBED_AS.equals(methodName); + } + + static boolean isNavigation(final String methodName) { + return NAVIGATION_METHODS.contains(methodName); + } +} diff --git a/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJOperation.java b/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJOperation.java new file mode 100644 index 00000000..d79b025f --- /dev/null +++ b/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJOperation.java @@ -0,0 +1,160 @@ +/* + * 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.assertj; + +import io.qameta.allure.model.Stage; +import io.qameta.allure.model.Parameter; +import io.qameta.allure.model.Status; +import io.qameta.allure.model.StatusDetails; +import io.qameta.allure.model.StepResult; + +import java.util.List; + +import static io.qameta.allure.util.ResultsUtils.getStatus; +import static io.qameta.allure.util.ResultsUtils.getStatusDetails; + +/** + * Child Allure step for one meaningful AssertJ fluent operation. + * + *

An operation is the report entry for one fluent method call inside an {@link AssertJChain}. The recorder creates + * it before proceeding with the intercepted AssertJ call, marks it passed or failed after the call returns, and keeps + * it attached to the chain that owns the assertion object. Earlier operations remain passed when a later operation + * fails, so the report shows the exact point where the assertion chain stopped matching the expectation.

+ * + *

For a simple assertion, each checked method becomes one operation:

+ *
{@code
+ * assertThat("Data").startsWith("Da").endsWith("ta")
+ *
+ * assert "Data"
+ *   starts with "Da"
+ *   ends with "ta"
+ * }
+ * + *

For navigation methods, the operation name is enriched with the returned subject. The returned AssertJ object + * still belongs to the same chain, so the report stays readable as one story:

+ *
{@code
+ * assertThat(users).first(InstanceOfAssertFactories.STRING).startsWith("alice")
+ *
+ * assert 1 string
+ *   first element as InstanceOfAssertFactory -> "alice@example.org"
+ *   starts with "alice"
+ * }
+ * + *

For failures, this operation receives the failure status and status details, and the parent chain receives the + * same status. This makes the failed operation visible without losing the successful context before it:

+ *
{@code
+ * assertThat("Data").startsWith("Da").hasSize(5)
+ *
+ * assert "Data"                 FAILED
+ *   starts with "Da"             PASSED
+ *   has size 5                   FAILED
+ * }
+ * + *

Some AssertJ methods call other assertion methods internally. Those calls should not become extra child steps + * because they would duplicate implementation details instead of user intent. The {@code nestedLevel} counter lets the + * recorder reuse the active operation while those internal calls run, then finish only the user-visible operation.

+ */ +final class AssertJOperation { + + private final AssertJChain chain; + + private final String methodName; + + private final StepResult step; + + private final boolean navigation; + + private String returnedSubject; + + private int nestedLevel; + + AssertJOperation(final AssertJChain chain, + final String methodName, + final String name, + final List parameters, + final boolean navigation) { + this.chain = chain; + this.methodName = methodName; + this.navigation = navigation; + this.step = new StepResult() + .setName(name) + .setParameters(parameters) + .setStage(Stage.RUNNING) + .setStart(System.currentTimeMillis()); + } + + AssertJChain getChain() { + return chain; + } + + StepResult getStep() { + return step; + } + + boolean isNavigation() { + return navigation; + } + + boolean isDescription() { + return AssertJMethodSupport.isDescription(methodName); + } + + boolean isNested() { + return nestedLevel > 0; + } + + AssertJOperation nested() { + nestedLevel++; + return this; + } + + void leaveNested() { + nestedLevel--; + } + + void setReturnedSubject(final String subject) { + if (returnedSubject != null) { + return; + } + + returnedSubject = subject; + step.setName(AssertJValueRenderer.truncateStepName(step.getName() + " -> " + subject)); + } + + void passed() { + if (step.getStatus() == null) { + step.setStatus(Status.PASSED); + } + finish(); + } + + void failed(final Throwable throwable) { + final Status status = getStatus(throwable).orElse(Status.BROKEN); + final StatusDetails details = getStatusDetails(throwable).orElse(null); + step + .setStatus(status) + .setStatusDetails(details); + chain.updateStatus(status, details); + finish(); + } + + private void finish() { + step + .setStage(Stage.FINISHED) + .setStop(System.currentTimeMillis()); + chain.finish(); + } +} diff --git a/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJRecorder.java b/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJRecorder.java new file mode 100644 index 00000000..423d5a48 --- /dev/null +++ b/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJRecorder.java @@ -0,0 +1,261 @@ +/* + * 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.assertj; + +import io.qameta.allure.AllureLifecycle; +import org.assertj.core.api.AbstractAssert; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Optional; + +/** + * Records AssertJ objects by identity and builds one Allure step tree per assertion chain. + * + *

The recorder is the stateful part behind {@link AllureAspectJ}. The aspect only detects user-side AssertJ + * factory calls and fluent operation calls; this class decides which {@link AssertJChain} owns the assertion object, + * where a new {@link AssertJOperation} should be attached, and how pass/fail state should be reflected in the retained + * Allure {@code StepResult} tree.

+ * + *

Each aspect thread gets its own recorder instance. Assertion objects are tracked in an {@link IdentityHashMap} + * because AssertJ assertion classes can override {@code equals} and {@code hashCode}; object identity is the only safe + * way to know that a later fluent call belongs to the same assertion object that was created earlier.

+ * + *

The normal hard-assertion flow is:

+ *
{@code
+ * assertThat("Data").startsWith("Da").endsWith("ta")
+ *
+ * assertionCreated(assertThat result, "Data")
+ * startOperation(startsWith, ["Da"])
+ * operationPassed(startsWith)
+ * startOperation(endsWith, ["ta"])
+ * operationPassed(endsWith)
+ *
+ * assert "Data"
+ *   starts with "Da"
+ *   ends with "ta"
+ * }
+ * + *

Stored assertion instances keep separate chains because the map key is the assertion instance itself:

+ *
{@code
+ * final AbstractStringAssert a = assertThat("alpha");
+ * final AbstractStringAssert b = assertThat("bravo");
+ *
+ * a.isEqualTo("alpha");
+ * b.isEqualTo("bravo");
+ *
+ * assert "alpha"
+ *   is equal to "alpha"
+ * assert "bravo"
+ *   is equal to "bravo"
+ * }
+ * + *

Navigation operations such as {@code extracting}, {@code first}, and {@code asInstanceOf} may return new AssertJ + * assertion objects. Those returned objects are registered against the existing chain, so later checks stay under the + * same parent step:

+ *
{@code
+ * assertThat(results).extracting(Result::getName).containsExactly("passed")
+ *
+ * assert 1 Result item
+ *   extracts Result::getName -> 1 string
+ *   contains exactly ["passed"]
+ * }
+ * + *

The {@code operations} stack tracks the currently executing user-visible operation. It has two jobs: assertions + * created inside callbacks such as {@code satisfies} are attached beneath the active operation, and AssertJ internal + * calls on the same chain are counted as nested work instead of being reported as extra steps.

+ * + *
{@code
+ * assertThat("alpha").satisfies(value -> assertThat(value).startsWith("al"))
+ *
+ * assert "alpha"
+ *   satisfies 
+ *     assert "alpha"
+ *       starts with "al"
+ * }
+ * + *

Soft assertion failures are reported before {@code assertAll()} throws. The AssertJ error collector callback calls + * {@link #softAssertionFailed(AssertionError)}, which marks the active operation and its chain as failed while + * preserving the earlier passed operations.

+ */ +final class AssertJRecorder { + + private final Map, AssertJChain> chains = new IdentityHashMap<>(); + + private final Deque operations = new ArrayDeque<>(); + + private final AssertJValueRenderer renderer = new AssertJValueRenderer(); + + void assertionCreated(final AllureLifecycle lifecycle, + final AbstractAssert assertion, + final Object actual) { + if (chains.containsKey(assertion)) { + return; + } + + final AssertJOperation activeOperation = activeOperation(); + if (isNavigationResult(activeOperation)) { + chains.put(assertion, activeOperation.getChain()); + return; + } + + final AssertJChain chain = new AssertJChain(assertion, renderer.renderSubject(actual)); + chains.put(assertion, chain); + attachChain(lifecycle, chain, activeOperation); + } + + AssertJOperation startOperation(final AllureLifecycle lifecycle, + final AbstractAssert assertion, + final String methodName, + final Object... args) { + final AssertJChain chain = chainFor(lifecycle, assertion); + final String normalizedName = AssertJMethodSupport.normalize(methodName); + + final AssertJOperation activeOperation = activeOperation(); + if (isInternalCallOnSameChain(activeOperation, chain)) { + return activeOperation.nested(); + } + + final AssertJOperation operation = new AssertJOperation( + chain, + normalizedName, + renderer.renderOperation(normalizedName, args), + renderer.renderParameters(normalizedName, args), + AssertJMethodSupport.isNavigation(normalizedName) + ); + chain.addOperation(operation); + operations.push(operation); + return operation; + } + + void operationPassed(final AssertJOperation operation, final Object result) { + if (operation.isNested()) { + pop(operation); + return; + } + + registerReturnedAssertion(operation, result); + renameChainFromDescription(operation); + operation.passed(); + pop(operation); + } + + void operationFailed(final AssertJOperation operation, final Throwable throwable) { + operation.failed(throwable); + pop(operation); + } + + void softAssertionFailed(final AssertionError error) { + final AssertJOperation current = activeOperation(); + if (current != null) { + current.failed(error); + } + } + + boolean isIgnored(final String methodName) { + return AssertJMethodSupport.isIgnored(methodName); + } + + private AssertJChain chainFor(final AllureLifecycle lifecycle, final AbstractAssert assertion) { + final AssertJChain chain = chains.get(assertion); + if (chain != null) { + return chain; + } + + final AssertJChain created = new AssertJChain(assertion, renderer.renderSubject(actualOf(assertion))); + chains.put(assertion, created); + attachChain(lifecycle, created, activeOperation()); + return created; + } + + private void attachChain(final AllureLifecycle lifecycle, + final AssertJChain chain, + final AssertJOperation parentOperation) { + if (parentOperation == null) { + lifecycle.startStep(chain.getUuid(), chain.getStep()); + lifecycle.stopStep(chain.getUuid()); + return; + } + + parentOperation.getStep().getSteps().add(chain.getStep()); + } + + private void registerReturnedAssertion(final AssertJOperation operation, final Object result) { + if (!(result instanceof AbstractAssert)) { + return; + } + + final AbstractAssert returned = (AbstractAssert) result; + chains.put(returned, operation.getChain()); + if (operation.isNavigation()) { + operation.setReturnedSubject(renderer.renderSubject(actualOf(returned))); + } + } + + private void renameChainFromDescription(final AssertJOperation operation) { + if (operation.isDescription()) { + operation.getChain().rename(descriptionOf(operation.getChain().getAssertion())); + } + } + + private AssertJOperation activeOperation() { + return operations.peek(); + } + + private boolean isNavigationResult(final AssertJOperation activeOperation) { + return activeOperation != null && activeOperation.isNavigation(); + } + + @SuppressWarnings("PMD.CompareObjectsWithEquals") + private boolean isInternalCallOnSameChain(final AssertJOperation activeOperation, final AssertJChain chain) { + return activeOperation != null && activeOperation.getChain() == chain; + } + + @SuppressWarnings("PMD.CompareObjectsWithEquals") + private void pop(final AssertJOperation operation) { + if (operation.isNested()) { + operation.leaveNested(); + return; + } + if (!operations.isEmpty() && operations.peek() == operation) { + operations.pop(); + } + } + + private Object actualOf(final AbstractAssert assertion) { + return AllureAspectJ.withoutRecording(() -> { + try { + return assertion.actual(); + } catch (RuntimeException e) { + return null; + } + }); + } + + private Optional descriptionOf(final AbstractAssert assertion) { + return AllureAspectJ.withoutRecording(() -> { + try { + return Optional.ofNullable(assertion.descriptionText()) + .map(String::trim) + .filter(value -> !value.isEmpty()); + } catch (RuntimeException e) { + return Optional.empty(); + } + }); + } +} diff --git a/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJValueRenderer.java b/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJValueRenderer.java new file mode 100644 index 00000000..d7bee7c8 --- /dev/null +++ b/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJValueRenderer.java @@ -0,0 +1,556 @@ +/* + * 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.assertj; + +import io.qameta.allure.model.Parameter; +import io.qameta.allure.util.ObjectUtils; +import org.assertj.core.description.Description; +import org.assertj.core.groups.Tuple; + +import java.lang.reflect.Array; +import java.net.URI; +import java.net.URL; +import java.nio.file.Path; +import java.time.temporal.TemporalAccessor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import static io.qameta.allure.util.ResultsUtils.getLambdaName; + +/** + * Renders AssertJ subjects and arguments into semantic step names. + */ +@SuppressWarnings("all") +final class AssertJValueRenderer { + + private static final int STEP_NAME_LIMIT = 1000; + + private static final int INLINE_VALUE_LIMIT = 3; + + private static final String LAMBDA = ""; + + private static final String TRUNCATED = "..."; + + String renderSubject(final Object value) { + return truncateStepName(renderSubjectValue(value)); + } + + String renderOperation(final String methodName, final Object[] args) { + return truncateStepName(renderOperationName(methodName, args)); + } + + List renderParameters(final String methodName, final Object[] args) { + final Object[] values = parameterArguments(methodName, args); + if (values.length == 0) { + return Collections.emptyList(); + } + + final String renderedOperation = renderOperation(methodName, args); + final List parameters = new ArrayList<>(); + for (int index = 0; index < values.length; index++) { + final String value = renderParameterValue(values[index]); + if (renderedOperation.contains(value)) { + continue; + } + parameters.add(new Parameter() + .setName(parameterName(methodName, index)) + .setValue(value) + .setMode(Parameter.Mode.DEFAULT)); + } + return parameters; + } + + static String truncateStepName(final String value) { + if (value == null || value.length() <= STEP_NAME_LIMIT) { + return value; + } + return value.substring(0, STEP_NAME_LIMIT - TRUNCATED.length()) + TRUNCATED; + } + + private String renderSubjectValue(final Object value) { + if (value == null) { + return "null"; + } + if (value instanceof CharSequence || isSimple(value)) { + return renderSimple(value); + } + if (value instanceof Collection) { + if (isInlineCollection((Collection) value)) { + return renderCollectionValue((Collection) value); + } + return renderCollectionSubject((Collection) value); + } + if (value instanceof Map) { + return "map with " + renderEntryCount(((Map) value).size()); + } + if (value.getClass().isArray()) { + return renderArraySubject(value); + } + if (value instanceof Iterable) { + return "iterable"; + } + return simpleClassName(value); + } + + private Object[] parameterArguments(final String methodName, final Object[] args) { + if (isDescriptionWithEmptyValues(args)) { + return new Object[]{args[0]}; + } + if (isSingleVarargToUnwrap(methodName, args)) { + return new Object[]{Array.get(args[0], 0)}; + } + return args; + } + + private String parameterName(final String methodName, final int index) { + if ("hasFieldOrPropertyWithValue".equals(methodName)) { + return index == 0 ? "field or property" : "expected value"; + } + + if (index > 0) { + return "argument " + (index + 1); + } + + switch (methodName) { + case "as": + return "description"; + case "asInstanceOf": + case "first": + case "singleElement": + return "factory"; + case "extracting": + case "flatExtracting": + return "extractor"; + case "hasSize": + return "expected size"; + case "satisfies": + return "condition"; + case "contains": + case "containsExactly": + case "containsExactlyInAnyOrder": + case "endsWith": + case "isEqualTo": + case "startsWith": + return "expected"; + default: + return "argument 1"; + } + } + + private String renderParameterValue(final Object value) { + return renderArgument(value); + } + + private String renderOperationName(final String methodName, final Object[] args) { + if (args.length == 0) { + return readableMethodName(methodName); + } + + final String arguments = renderArguments(methodName, args); + switch (methodName) { + case "as": + return "described as " + arguments; + case "asInstanceOf": + return "as instance of " + arguments; + case "contains": + return "contains " + arguments; + case "containsExactly": + return "contains exactly " + arguments; + case "containsExactlyInAnyOrder": + return "contains exactly in any order " + arguments; + case "endsWith": + return "ends with " + arguments; + case "extracting": + return "extracts " + arguments; + case "flatExtracting": + return "flat extracts " + arguments; + case "first": + return "first element as " + arguments; + case "hasFieldOrPropertyWithValue": + return renderHasFieldOrPropertyWithValue(args); + case "hasSize": + return "has size " + arguments; + case "isEqualTo": + return "is equal to " + arguments; + case "singleElement": + return "single element as " + arguments; + case "startsWith": + return "starts with " + arguments; + case "satisfies": + return "satisfies " + arguments; + default: + return readableMethodName(methodName) + " " + arguments; + } + } + + private String renderHasFieldOrPropertyWithValue(final Object[] args) { + if (args.length != 2) { + return "has field or property with value " + renderEach(args); + } + return "has field or property " + renderArgument(args[0]) + " with value " + renderArgument(args[1]); + } + + private String readableMethodName(final String methodName) { + if (methodName.startsWith("is") && methodName.length() > 2 && Character.isUpperCase(methodName.charAt(2))) { + return "is " + splitCamelCase(methodName.substring(2)); + } + if (methodName.startsWith("has") && methodName.length() > 3 && Character.isUpperCase(methodName.charAt(3))) { + return "has " + splitCamelCase(methodName.substring(3)); + } + return splitCamelCase(methodName); + } + + private String splitCamelCase(final String value) { + return value + .replaceAll("([a-z0-9])([A-Z])", "$1 $2") + .toLowerCase(); + } + + private String renderArguments(final String methodName, final Object[] args) { + if (isDescriptionWithEmptyValues(args)) { + return renderArgument(args[0]); + } + if (isSingleVarargToUnwrap(methodName, args)) { + return renderArgument(Array.get(args[0], 0)); + } + if (isSingleArrayArgument(args)) { + return renderArray(args[0]); + } + return renderEach(args); + } + + private boolean isDescriptionWithEmptyValues(final Object[] args) { + return args.length == 2 + && args[1] != null + && args[1].getClass().isArray() + && Array.getLength(args[1]) == 0; + } + + private boolean isSingleVarargToUnwrap(final String methodName, final Object[] args) { + return args.length == 1 + && args[0] != null + && args[0].getClass().isArray() + && Array.getLength(args[0]) == 1 + && shouldUnwrapSingleVararg(methodName); + } + + private boolean isSingleArrayArgument(final Object[] args) { + return args.length == 1 + && args[0] != null + && args[0].getClass().isArray(); + } + + private boolean shouldUnwrapSingleVararg(final String methodName) { + return !methodName.contains("Any") + && !methodName.contains("Exactly") + && !methodName.contains("Only") + && !methodName.contains("Sequence") + && !methodName.contains("Subsequence") + && !methodName.endsWith("In"); + } + + private String renderEach(final Object[] args) { + final List values = new ArrayList<>(); + for (Object arg : args) { + values.add(renderArgument(arg)); + } + return values.stream().collect(Collectors.joining(", ")); + } + + private String renderArgument(final Object value) { + if (value == null) { + return "null"; + } + if (isLambda(value)) { + return renderLambda(value); + } + if (value instanceof Description) { + return renderSimple(value.toString()); + } + if (value instanceof Tuple) { + return renderTuple((Tuple) value); + } + if (value instanceof CharSequence || isSimple(value)) { + return renderSimple(value); + } + if (value instanceof Collection) { + if (isInlineCollection((Collection) value)) { + return renderCollectionValue((Collection) value); + } + return renderCollectionSubject((Collection) value); + } + if (value instanceof Map) { + return "map with " + renderEntryCount(((Map) value).size()); + } + if (value.getClass().isArray()) { + return renderArray(value); + } + return simpleClassName(value); + } + + private String renderArray(final Object array) { + if (array instanceof byte[]) { + return ObjectUtils.toString(array); + } + + final int length = Array.getLength(array); + if (array.getClass().getComponentType().isPrimitive()) { + return ObjectUtils.toString(array); + } + if (allLambdas(array, length)) { + return length == 1 ? renderLambda(Array.get(array, 0)) : lambdaList(array, length); + } + if (allSimple(array, length) || !array.getClass().getComponentType().isPrimitive()) { + return renderObjectArray(array, length); + } + return array.getClass().getComponentType().getSimpleName() + "[](length=" + length + ")"; + } + + private String renderCollectionSubject(final Collection value) { + final int size = value.size(); + if (size == 0) { + return "empty collection"; + } + return commonElementType(value) + .map(type -> renderElementCount(size, type)) + .orElseGet(() -> renderItemCount(size)); + } + + private String renderArraySubject(final Object array) { + final int length = Array.getLength(array); + if (isInlineArray(array, length)) { + return renderArrayValue(array, length); + } + if (array instanceof byte[]) { + return "byte array with " + renderByteCount(length); + } + return renderElementCount(length, array.getClass().getComponentType()); + } + + private boolean isInlineCollection(final Collection value) { + return value.size() <= INLINE_VALUE_LIMIT && allInlineValues(value); + } + + private boolean allInlineValues(final Collection value) { + for (Object item : value) { + if (!isInlineValue(item)) { + return false; + } + } + return true; + } + + private boolean isInlineValue(final Object value) { + return value == null + || isLambda(value) + || value instanceof Description + || isInlineTuple(value) + || value instanceof CharSequence + || isSimple(value); + } + + private boolean isInlineTuple(final Object value) { + if (!(value instanceof Tuple)) { + return false; + } + final Object[] values = ((Tuple) value).toArray(); + return isInlineArray(values, values.length); + } + + private String renderTuple(final Tuple tuple) { + final Object[] values = tuple.toArray(); + return renderObjectArray(values, values.length) + .replaceFirst("^\\[", "(") + .replaceFirst("]$", ")"); + } + + private String renderCollectionValue(final Collection value) { + final List values = new ArrayList<>(); + for (Object item : value) { + values.add(renderArgument(item)); + } + return values.stream().collect(Collectors.joining(", ", "[", "]")); + } + + private boolean isInlineArray(final Object array, final int length) { + if (length > INLINE_VALUE_LIMIT || array instanceof byte[]) { + return false; + } + if (array.getClass().getComponentType().isPrimitive()) { + return true; + } + for (int i = 0; i < length; i++) { + if (!isInlineValue(Array.get(array, i))) { + return false; + } + } + return true; + } + + private String renderArrayValue(final Object array, final int length) { + if (array.getClass().getComponentType().isPrimitive()) { + return ObjectUtils.toString(array); + } + return renderObjectArray(array, length); + } + + private Optional> commonElementType(final Collection value) { + Class result = null; + for (Object item : value) { + if (item == null) { + continue; + } + final Class itemType = elementTypeOf(item); + if (result == null) { + result = itemType; + } else if (!result.equals(itemType)) { + return java.util.Optional.empty(); + } + } + return java.util.Optional.ofNullable(result); + } + + private Class elementTypeOf(final Object item) { + if (item instanceof Collection) { + return Collection.class; + } + if (item instanceof Map) { + return Map.class; + } + return item.getClass(); + } + + private String renderElementCount(final int size, final Class type) { + if (String.class.equals(type)) { + return size + " " + pluralize("string", size); + } + if (Boolean.class.equals(type) || Boolean.TYPE.equals(type)) { + return size + " " + pluralize("boolean", size); + } + if (Character.class.equals(type) || Character.TYPE.equals(type)) { + return size + " " + pluralize("character", size); + } + if (Number.class.isAssignableFrom(type) || type.isPrimitive() && !Boolean.TYPE.equals(type) + && !Character.TYPE.equals(type)) { + return size + " " + pluralize("number", size); + } + if (Collection.class.equals(type)) { + return size + " " + pluralize("collection", size); + } + if (Map.class.equals(type)) { + return size + " " + pluralize("map", size); + } + return size + " " + type.getSimpleName() + " " + pluralize("item", size); + } + + private String renderItemCount(final int size) { + return size + " " + pluralize("item", size); + } + + private String renderEntryCount(final int size) { + return size + " " + pluralize("entry", size); + } + + private String renderByteCount(final int size) { + return size + " " + pluralize("byte", size); + } + + private String pluralize(final String word, final int count) { + return count == 1 ? word : word + "s"; + } + + private String renderObjectArray(final Object array, final int length) { + final List values = new ArrayList<>(); + for (int i = 0; i < length; i++) { + values.add(renderArgument(Array.get(array, i))); + } + return values.stream().collect(Collectors.joining(", ", "[", "]")); + } + + private boolean allLambdas(final Object array, final int length) { + if (length == 0) { + return false; + } + for (int i = 0; i < length; i++) { + if (!isLambda(Array.get(array, i))) { + return false; + } + } + return true; + } + + private String lambdaList(final Object array, final int length) { + final List values = new ArrayList<>(); + for (int i = 0; i < length; i++) { + values.add(renderLambda(Array.get(array, i))); + } + return values.stream().collect(Collectors.joining(", ", "[", "]")); + } + + private boolean allSimple(final Object array, final int length) { + for (int i = 0; i < length; i++) { + final Object item = Array.get(array, i); + if (item != null && !isSimple(item) && !(item instanceof CharSequence)) { + return false; + } + } + return true; + } + + private boolean isSimple(final Object value) { + return value instanceof Number + || value instanceof Boolean + || value instanceof Character + || value instanceof Enum + || value instanceof Path + || value instanceof URI + || value instanceof URL + || value instanceof TemporalAccessor; + } + + private boolean isLambda(final Object value) { + final Class type = value.getClass(); + return type.isSynthetic() || type.getName().contains("$$Lambda$"); + } + + private String renderLambda(final Object value) { + return getLambdaName(value) + .orElse(LAMBDA); + } + + private String renderSimple(final Object value) { + if (value instanceof CharSequence || value instanceof Character) { + return "\"" + ObjectUtils.toString(value) + "\""; + } + if (value instanceof Enum) { + return ((Enum) value).name(); + } + return ObjectUtils.toString(value); + } + + private String simpleClassName(final Object value) { + final Class type = value.getClass(); + if (type.isAnonymousClass()) { + return type.getSuperclass().getSimpleName(); + } + return type.getSimpleName(); + } +} diff --git a/allure-assertj/src/main/resources/META-INF/services/io.qameta.allure.listener.FixtureLifecycleListener b/allure-assertj/src/main/resources/META-INF/services/io.qameta.allure.listener.FixtureLifecycleListener new file mode 100644 index 00000000..65aaf3ec --- /dev/null +++ b/allure-assertj/src/main/resources/META-INF/services/io.qameta.allure.listener.FixtureLifecycleListener @@ -0,0 +1 @@ +io.qameta.allure.assertj.AssertJLifecycleListener diff --git a/allure-assertj/src/main/resources/META-INF/services/io.qameta.allure.listener.TestLifecycleListener b/allure-assertj/src/main/resources/META-INF/services/io.qameta.allure.listener.TestLifecycleListener new file mode 100644 index 00000000..65aaf3ec --- /dev/null +++ b/allure-assertj/src/main/resources/META-INF/services/io.qameta.allure.listener.TestLifecycleListener @@ -0,0 +1 @@ +io.qameta.allure.assertj.AssertJLifecycleListener diff --git a/allure-assertj/src/test/java/io/qameta/allure/assertj/AllureAspectJTest.java b/allure-assertj/src/test/java/io/qameta/allure/assertj/AllureAspectJTest.java index 957657b1..1567e7e1 100644 --- a/allure-assertj/src/test/java/io/qameta/allure/assertj/AllureAspectJTest.java +++ b/allure-assertj/src/test/java/io/qameta/allure/assertj/AllureAspectJTest.java @@ -15,18 +15,27 @@ */ package io.qameta.allure.assertj; +import io.qameta.allure.model.Parameter; +import io.qameta.allure.model.Status; +import io.qameta.allure.model.StatusDetails; import io.qameta.allure.model.StepResult; import io.qameta.allure.model.TestResult; import io.qameta.allure.test.AllureFeatures; import io.qameta.allure.test.AllureResults; +import org.assertj.core.api.AbstractStringAssert; +import org.assertj.core.api.InstanceOfAssertFactories; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.Test; +import java.io.Serializable; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Collections; +import java.util.function.Function; import static io.qameta.allure.test.RunUtils.runWithinTestContext; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; /** * @author charlie (Dmitry Baev). @@ -35,90 +44,457 @@ class AllureAspectJTest { @AllureFeatures.Steps @Test - void shouldCreateStepsForAsserts() { + void shouldCreateSemanticChainForScalarAssert() { final AllureResults results = runWithinTestContext(() -> { assertThat("Data") .hasSize(4); }, AllureAspectJ::setLifecycle); - assertThat(results.getTestResults()) - .flatExtracting(TestResult::getSteps) + + final TestResult result = assertOnlyOneResult(results); + assertThat(result.getSteps()) + .extracting(StepResult::getName, StepResult::getStatus) + .containsExactly(tuple("assert \"Data\"", Status.PASSED)); + assertThat(result.getSteps()) + .flatExtracting(StepResult::getSteps) .extracting(StepResult::getName) - .containsExactly( - "assertThat 'Data'", - "hasSize '4'" - ); + .containsExactly("has size 4"); + assertThat(result.getSteps()) + .flatExtracting(StepResult::getSteps) + .flatExtracting(StepResult::getParameters) + .isEmpty(); } @AllureFeatures.Steps @Test - void shouldHandleNullableObject() { + void shouldUseAssertDescriptionAsChainName() { final AllureResults results = runWithinTestContext(() -> { assertThat((Object) null) .as("Nullable object") .isNull(); }, AllureAspectJ::setLifecycle); - assertThat(results.getTestResults()) - .flatExtracting(TestResult::getSteps) + final TestResult result = assertOnlyOneResult(results); + assertThat(result.getSteps()) + .extracting(StepResult::getName) + .containsExactly("assert Nullable object"); + assertThat(result.getSteps()) + .flatExtracting(StepResult::getSteps) + .extracting(StepResult::getName) + .containsExactly("described as \"Nullable object\"", "is null"); + } + + @AllureFeatures.Steps + @Test + void shouldRenderByteArraysWithoutPayload() { + final String value = "some string"; + final AllureResults results = runWithinTestContext(() -> { + assertThat(value.getBytes(StandardCharsets.UTF_8)) + .as("Byte array object") + .isEqualTo(value.getBytes(StandardCharsets.UTF_8)); + }, AllureAspectJ::setLifecycle); + + final TestResult result = assertOnlyOneResult(results); + assertThat(result.getSteps()) + .extracting(StepResult::getName) + .containsExactly("assert Byte array object"); + assertThat(result.getSteps()) + .flatExtracting(StepResult::getSteps) + .extracting(StepResult::getName) + .containsExactly("described as \"Byte array object\"", "is equal to "); + } + + @AllureFeatures.Steps + @Test + void shouldRenderCollectionsAsSubjectsAndExpectedValuesAsValues() { + final AllureResults results = runWithinTestContext(() -> { + assertThat(Arrays.asList("a", "b")) + .containsExactly("a", "b"); + }, AllureAspectJ::setLifecycle); + + final TestResult result = assertOnlyOneResult(results); + assertThat(result.getSteps()) + .extracting(StepResult::getName) + .containsExactly("assert [\"a\", \"b\"]"); + assertThat(result.getSteps()) + .flatExtracting(StepResult::getSteps) + .extracting(StepResult::getName) + .containsExactly("contains exactly [\"a\", \"b\"]"); + assertThat(result.getSteps()) + .flatExtracting(StepResult::getSteps) + .flatExtracting(StepResult::getParameters) + .isEmpty(); + } + + @AllureFeatures.Steps + @Test + void shouldRenderSmallArraysAsValues() { + final AllureResults results = runWithinTestContext(() -> { + assertThat(new int[]{1, 2}) + .containsExactly(1, 2); + + assertThat(new String[]{"alpha", "bravo"}) + .containsExactly("alpha", "bravo"); + }, AllureAspectJ::setLifecycle); + + final TestResult result = assertOnlyOneResult(results); + assertThat(result.getSteps()) + .extracting(StepResult::getName) + .containsExactly("assert [1, 2]", "assert [\"alpha\", \"bravo\"]"); + assertThat(result.getSteps()) + .flatExtracting(StepResult::getSteps) + .extracting(StepResult::getName) + .containsExactly("contains exactly [1, 2]", "contains exactly [\"alpha\", \"bravo\"]"); + } + + @AllureFeatures.Steps + @Test + void shouldRenderTuplesAsValues() { + final AllureResults results = runWithinTestContext(() -> { + assertThat(Arrays.asList( + tuple("first", Status.PASSED), + tuple("second", Status.FAILED) + )) + .containsExactly( + tuple("first", Status.PASSED), + tuple("second", Status.FAILED) + ); + }, AllureAspectJ::setLifecycle); + + final TestResult result = assertOnlyOneResult(results); + assertThat(result.getSteps()) + .extracting(StepResult::getName) + .containsExactly("assert [(\"first\", PASSED), (\"second\", FAILED)]"); + assertThat(result.getSteps()) + .flatExtracting(StepResult::getSteps) + .extracting(StepResult::getName) + .containsExactly("contains exactly [(\"first\", PASSED), (\"second\", FAILED)]"); + assertThat(result.getSteps()) + .flatExtracting(StepResult::getSteps) + .flatExtracting(StepResult::getParameters) + .isEmpty(); + } + + @AllureFeatures.Steps + @Test + void shouldRenderFieldOrPropertyValueAssertions() { + final StatusDetails details = new StatusDetails() + .setMessage("Make the test failed"); + + final AllureResults results = runWithinTestContext(() -> { + assertThat(details) + .hasFieldOrPropertyWithValue("message", "Make the test failed"); + }, AllureAspectJ::setLifecycle); + + final TestResult result = assertOnlyOneResult(results); + assertThat(result.getSteps()) + .extracting(StepResult::getName) + .containsExactly("assert StatusDetails"); + assertThat(result.getSteps()) + .flatExtracting(StepResult::getSteps) + .extracting(StepResult::getName) + .containsExactly("has field or property \"message\" with value \"Make the test failed\""); + assertThat(result.getSteps()) + .flatExtracting(StepResult::getSteps) + .flatExtracting(StepResult::getParameters) + .isEmpty(); + } + + @AllureFeatures.Steps + @Test + void shouldTruncateLongStepNamesAndAddOnlyTruncatedValuesAsParameters() { + final String value = String.join("", Collections.nCopies(1200, "a")); + + final AllureResults results = runWithinTestContext(() -> { + assertThat(value) + .isEqualTo(value); + }, AllureAspectJ::setLifecycle); + + final TestResult result = assertOnlyOneResult(results); + assertThat(result.getSteps()) + .extracting(StepResult::getName) + .singleElement() + .asString() + .hasSize(1000) + .startsWith("assert \"") + .endsWith("..."); + assertThat(result.getSteps()) + .flatExtracting(StepResult::getSteps) + .extracting(StepResult::getName) + .singleElement() + .asString() + .hasSize(1000) + .startsWith("is equal to \"") + .endsWith("..."); + assertThat(result.getSteps()) + .flatExtracting(StepResult::getSteps) + .flatExtracting(StepResult::getParameters) + .extracting(Parameter::getName, Parameter::getValue) + .containsExactly(tuple("expected", "\"" + value + "\"")); + } + + @AllureFeatures.Steps + @Test + void shouldCreateSeparateChainsForMultipleAssertThatCalls() { + final AllureResults results = runWithinTestContext(() -> { + assertThat("Data") + .hasSize(4); + + assertThat(42) + .isPositive() + .isEqualTo(42); + + assertThat(Arrays.asList("a", "b")) + .hasSize(2) + .contains("a"); + }, AllureAspectJ::setLifecycle); + + final TestResult result = assertOnlyOneResult(results); + assertThat(result.getSteps()) + .extracting(StepResult::getName, StepResult::getStatus) + .containsExactly( + tuple("assert \"Data\"", Status.PASSED), + tuple("assert 42", Status.PASSED), + tuple("assert [\"a\", \"b\"]", Status.PASSED) + ); + assertThat(result.getSteps()) + .flatExtracting(StepResult::getSteps) .extracting(StepResult::getName) .containsExactly( - "assertThat 'null'", - "as 'Nullable object []'", - "isNull" + "has size 4", + "is positive", + "is equal to 42", + "has size 2", + "contains \"a\"" ); } @AllureFeatures.Steps @Test - void shouldHandleByteArrayObject() { - final String s = "some string"; + void shouldAttachOperationsToStoredAssertionInstances() { + final String targetA = "alpha"; + final String targetB = "bravo"; + final AllureResults results = runWithinTestContext(() -> { - assertThat(s.getBytes(StandardCharsets.UTF_8)) - .as("Byte array object") - .isEqualTo(s.getBytes(StandardCharsets.UTF_8)); + final AbstractStringAssert a = assertThat(targetA); + final AbstractStringAssert b = assertThat(targetB); + + a.isEqualTo("alpha"); + b.isEqualTo("bravo"); + }, AllureAspectJ::setLifecycle); + + final TestResult result = assertOnlyOneResult(results); + assertThat(result.getSteps()) + .extracting(StepResult::getName, StepResult::getStatus) + .containsExactly( + tuple("assert \"alpha\"", Status.PASSED), + tuple("assert \"bravo\"", Status.PASSED) + ); + assertThat(result.getSteps()) + .filteredOn("name", "assert \"alpha\"") + .flatExtracting(StepResult::getSteps) + .extracting(StepResult::getName) + .containsExactly("is equal to \"alpha\""); + assertThat(result.getSteps()) + .filteredOn("name", "assert \"bravo\"") + .flatExtracting(StepResult::getSteps) + .extracting(StepResult::getName) + .containsExactly("is equal to \"bravo\""); + } + + @AllureFeatures.Steps + @Test + void shouldAvoidVerboseModelToStringPayloads() { + final TestResult model = new TestResult() + .setUuid("uid") + .setName("testPassed") + .setFullName("other.PassingTest.testPassed"); + + final AllureResults results = runWithinTestContext(() -> { + assertThat(Collections.singletonList(model)) + .hasSize(1) + .containsExactly(model); + }, AllureAspectJ::setLifecycle); + + final TestResult result = assertOnlyOneResult(results); + assertThat(result.getSteps()) + .extracting(StepResult::getName) + .containsExactly("assert 1 TestResult item"); + assertThat(result.getSteps()) + .flatExtracting(StepResult::getSteps) + .extracting(StepResult::getName) + .containsExactly("has size 1", "contains exactly [TestResult]"); + assertThat(result.getSteps()) + .extracting(StepResult::getName) + .noneMatch(name -> name.contains("fullName=")) + .noneMatch(name -> name.contains("other.PassingTest.testPassed")); + assertThat(result.getSteps()) + .flatExtracting(StepResult::getSteps) + .extracting(StepResult::getName) + .noneMatch(name -> name.contains("fullName=")) + .noneMatch(name -> name.contains("other.PassingTest.testPassed")); + } + + @AllureFeatures.Steps + @Test + void shouldKeepNavigationInsideTheSameChain() { + final TestResult model = new TestResult() + .setFullName("my.company.Test.testOne"); + + final AllureResults results = runWithinTestContext(() -> { + assertThat(Collections.singletonList(model)) + .extracting(TestResult::getFullName) + .containsExactly("my.company.Test.testOne"); + + assertThat(Collections.singletonList("alpha")) + .first(InstanceOfAssertFactories.STRING) + .startsWith("al"); + + assertThat(Collections.singletonList("bravo")) + .singleElement(InstanceOfAssertFactories.STRING) + .endsWith("vo"); + + assertThat((Object) "charlie") + .asInstanceOf(InstanceOfAssertFactories.STRING) + .contains("har"); + + assertThat(Collections.singletonList(Collections.singletonList("delta"))) + .flatExtracting(value -> value) + .containsExactly("delta"); }, AllureAspectJ::setLifecycle); - assertThat(results.getTestResults()) - .flatExtracting(TestResult::getSteps) + final TestResult result = assertOnlyOneResult(results); + assertThat(result.getSteps()) + .hasSize(5); + assertThat(result.getSteps()) + .flatExtracting(StepResult::getSteps) .extracting(StepResult::getName) .containsExactly( - "assertThat ''", - "describedAs 'Byte array object'", - "isEqualTo ''" + "extracts -> [\"my.company.Test.testOne\"]", + "contains exactly [\"my.company.Test.testOne\"]", + "first element as InstanceOfAssertFactory -> \"alpha\"", + "starts with \"al\"", + "single element as InstanceOfAssertFactory -> \"bravo\"", + "ends with \"vo\"", + "as instance of InstanceOfAssertFactory -> \"charlie\"", + "contains \"har\"", + "flat extracts -> [\"delta\"]", + "contains exactly [\"delta\"]" ); } @AllureFeatures.Steps @Test - void shouldHandleCollections() { + void shouldRenderSerializedLambdaMethodReferences() { + final TestResult model = new TestResult() + .setFullName("my.company.Test.testOne"); + final AllureResults results = runWithinTestContext(() -> { - assertThat(Arrays.asList("a", "b")) - .containsExactly("a", "b"); + assertThat(Collections.singletonList(model)) + .extracting((Function & Serializable) TestResult::getFullName) + .containsExactly("my.company.Test.testOne"); + }, AllureAspectJ::setLifecycle); + + final TestResult result = assertOnlyOneResult(results); + assertThat(result.getSteps()) + .flatExtracting(StepResult::getSteps) + .extracting(StepResult::getName) + .containsExactly( + "extracts TestResult::getFullName -> [\"my.company.Test.testOne\"]", + "contains exactly [\"my.company.Test.testOne\"]" + ); + } + + @AllureFeatures.Steps + @Test + void shouldMarkTheFailedHardAssertionOperation() { + final AllureResults results = runWithinTestContext(() -> { + assertThat("Data") + .hasSize(5); }, AllureAspectJ::setLifecycle); - assertThat(results.getTestResults()) - .flatExtracting(TestResult::getSteps) - .extracting(StepResult::getName) - .containsExactly( - "assertThatList '[a, b]'", - "containsExactly '[a, b]'" - ); + final TestResult result = assertOnlyOneResult(results); + assertThat(result.getSteps()) + .extracting(StepResult::getName, StepResult::getStatus) + .containsExactly(tuple("assert \"Data\"", Status.FAILED)); + assertThat(result.getSteps()) + .flatExtracting(StepResult::getSteps) + .extracting(StepResult::getName, StepResult::getStatus) + .containsExactly(tuple("has size 5", Status.FAILED)); + assertThat(result.getSteps()) + .flatExtracting(StepResult::getSteps) + .filteredOn("name", "has size 5") + .extracting(step -> step.getStatusDetails().getMessage()) + .singleElement() + .asString() + .contains("size"); } @AllureFeatures.Steps @Test - void softAssertions() { + void shouldMarkTheFailedSoftAssertionOperationBeforeAssertAll() { final AllureResults results = runWithinTestContext(() -> { final SoftAssertions soft = new SoftAssertions(); soft.assertThat(25) - .as("Test description") + .as("Age") .isEqualTo(26); soft.assertAll(); }, AllureAspectJ::setLifecycle); - assertThat(results.getTestResults()) - .flatExtracting(TestResult::getSteps) + final TestResult result = assertOnlyOneResult(results); + assertThat(result.getSteps()) + .extracting(StepResult::getName, StepResult::getStatus) + .containsExactly(tuple("assert Age", Status.FAILED)); + assertThat(result.getSteps()) + .flatExtracting(StepResult::getSteps) + .extracting(StepResult::getName, StepResult::getStatus) + .containsExactly( + tuple("described as \"Age\"", Status.PASSED), + tuple("is equal to 26", Status.FAILED) + ); + assertThat(result.getSteps()) + .flatExtracting(StepResult::getSteps) + .filteredOn("name", "is equal to 26") + .extracting(step -> step.getStatusDetails().getMessage()) + .singleElement() + .asString() + .contains("expected: 26"); + } + + @AllureFeatures.Steps + @Test + void shouldAttachNestedAssertionsUnderCallbackOperations() { + final AllureResults results = runWithinTestContext(() -> { + assertThat("alpha") + .satisfies(value -> assertThat(value) + .startsWith("al") + .endsWith("ha")); + }, AllureAspectJ::setLifecycle); + + final TestResult result = assertOnlyOneResult(results); + assertThat(result.getSteps()) + .extracting(StepResult::getName) + .containsExactly("assert \"alpha\""); + assertThat(result.getSteps()) + .flatExtracting(StepResult::getSteps) .extracting(StepResult::getName) - .contains("as 'Test description []'", "isEqualTo '26'"); + .containsExactly("satisfies "); + assertThat(result.getSteps()) + .flatExtracting(StepResult::getSteps) + .filteredOn("name", "satisfies ") + .flatExtracting(StepResult::getSteps) + .extracting(StepResult::getName) + .containsExactly("assert \"alpha\""); + assertThat(result.getSteps()) + .flatExtracting(StepResult::getSteps) + .filteredOn("name", "satisfies ") + .flatExtracting(StepResult::getSteps) + .flatExtracting(StepResult::getSteps) + .extracting(StepResult::getName) + .containsExactly("starts with \"al\"", "ends with \"ha\""); + } + + private TestResult assertOnlyOneResult(final AllureResults results) { + assertThat(results.getTestResults()).hasSize(1); + return results.getTestResults().get(0); } } diff --git a/allure-java-commons/src/main/java/io/qameta/allure/util/ObjectUtils.java b/allure-java-commons/src/main/java/io/qameta/allure/util/ObjectUtils.java index f409d81b..f37ab44b 100644 --- a/allure-java-commons/src/main/java/io/qameta/allure/util/ObjectUtils.java +++ b/allure-java-commons/src/main/java/io/qameta/allure/util/ObjectUtils.java @@ -51,7 +51,9 @@ public static String toString(final Object object) { try { if (Objects.nonNull(object) && object.getClass().isArray()) { if (object instanceof Object[]) { - return Arrays.toString((Object[]) object); + return Arrays.stream((Object[]) object) + .map(ObjectUtils::toString) + .collect(Collectors.joining(", ", "[", "]")); } else if (object instanceof long[]) { return Arrays.toString((long[]) object); } else if (object instanceof short[]) { diff --git a/allure-java-commons/src/main/java/io/qameta/allure/util/ResultsUtils.java b/allure-java-commons/src/main/java/io/qameta/allure/util/ResultsUtils.java index d8d3b7f8..38faa3a5 100644 --- a/allure-java-commons/src/main/java/io/qameta/allure/util/ResultsUtils.java +++ b/allure-java-commons/src/main/java/io/qameta/allure/util/ResultsUtils.java @@ -35,6 +35,7 @@ import java.io.InputStream; import java.io.PrintWriter; import java.io.StringWriter; +import java.lang.invoke.SerializedLambda; import java.lang.management.ManagementFactory; import java.lang.reflect.Method; import java.math.BigInteger; @@ -336,6 +337,11 @@ public static String md5(final String source) { return bytesToHex(getMd5Digest().digest(source.getBytes(StandardCharsets.UTF_8))); } + public static Optional getLambdaName(final Object lambda) { + return getSerializedLambda(lambda) + .flatMap(ResultsUtils::formatLambdaName); + } + public static String bytesToHex(final byte[] bytes) { return new BigInteger(1, bytes).toString(16); } @@ -442,4 +448,45 @@ private static boolean separateLines() { return parseBoolean(loadAllureProperties().getProperty(ALLURE_SEPARATE_LINES_SYSPROP)); } + @SuppressWarnings("PMD.AvoidAccessibilityAlteration") + private static Optional getSerializedLambda(final Object value) { + if (Objects.isNull(value)) { + return Optional.empty(); + } + try { + final Method writeReplace = value.getClass().getDeclaredMethod("writeReplace"); + writeReplace.setAccessible(true); + final Object replacement = writeReplace.invoke(value); + if (replacement instanceof SerializedLambda) { + return Optional.of((SerializedLambda) replacement); + } + } catch (ReflectiveOperationException | RuntimeException ignored) { + return Optional.empty(); + } + return Optional.empty(); + } + + private static Optional formatLambdaName(final SerializedLambda lambda) { + final String methodName = lambda.getImplMethodName(); + if (methodName.startsWith("lambda$")) { + return Optional.empty(); + } + return Optional.of(simpleClassName(lambda.getImplClass()) + "::" + getLambdaMethodName(methodName)); + } + + private static String getLambdaMethodName(final String methodName) { + if ("".equals(methodName)) { + return "new"; + } + return methodName; + } + + private static String simpleClassName(final String name) { + final String normalized = name.replace('/', '.'); + final int packageIndex = normalized.lastIndexOf('.'); + final int nestedClassIndex = normalized.lastIndexOf('$'); + final int index = Math.max(packageIndex, nestedClassIndex); + return index < 0 ? normalized : normalized.substring(index + 1); + } + } diff --git a/allure-java-commons/src/test/java/io/qameta/allure/ResultsUtilsTest.java b/allure-java-commons/src/test/java/io/qameta/allure/ResultsUtilsTest.java index 79b6505a..dea5482c 100644 --- a/allure-java-commons/src/test/java/io/qameta/allure/ResultsUtilsTest.java +++ b/allure-java-commons/src/test/java/io/qameta/allure/ResultsUtilsTest.java @@ -24,8 +24,10 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import java.io.Serializable; import java.lang.annotation.Annotation; import java.util.Objects; +import java.util.function.Function; import java.util.stream.Stream; import static io.qameta.allure.util.ResultsUtils.ISSUE_LINK_TYPE; @@ -151,6 +153,32 @@ public String value() { .hasFieldOrPropertyWithValue("type", TMS_LINK_TYPE); } + @Test + void shouldGetSerializedLambdaName() { + final Function getter = + (Function & Serializable) LambdaSubject::getName; + + assertThat(ResultsUtils.getLambdaName(getter)) + .hasValue("LambdaSubject::getName"); + } + + @Test + void shouldIgnoreGeneratedSerializedLambdaBody() { + final Function getter = + (Function & Serializable) subject -> subject.getName(); + + assertThat(ResultsUtils.getLambdaName(getter)) + .isEmpty(); + } + + @Test + void shouldIgnoreNonSerializedLambda() { + final Function getter = LambdaSubject::getName; + + assertThat(ResultsUtils.getLambdaName(getter)) + .isEmpty(); + } + public static Stream data() { return Stream.of( Arguments.of("a", "b", "c", "d", "e", link("a", "c", "d")), @@ -200,4 +228,17 @@ public void clearSystemProperty(final String type, final String sysProp) { private static io.qameta.allure.model.Link link(String name, String url, String type) { return new io.qameta.allure.model.Link().setName(name).setUrl(url).setType(type); } + + private static final class LambdaSubject { + + private final String name; + + LambdaSubject(final String name) { + this.name = name; + } + + String getName() { + return name; + } + } } diff --git a/allure-java-commons/src/test/java/io/qameta/allure/util/ObjectUtilsTest.java b/allure-java-commons/src/test/java/io/qameta/allure/util/ObjectUtilsTest.java index e28ee2cf..586697b5 100644 --- a/allure-java-commons/src/test/java/io/qameta/allure/util/ObjectUtilsTest.java +++ b/allure-java-commons/src/test/java/io/qameta/allure/util/ObjectUtilsTest.java @@ -34,6 +34,20 @@ void shouldProcessToStringNpe() { .isEqualTo(""); } + @Test + void shouldProcessArraysItemByItem() { + final Object[] array = { + "value", + "binary".getBytes(), + new MyNpeClass(), + }; + + final String string = ObjectUtils.toString(array); + + assertThat(string) + .isEqualTo("[value, , ]"); + } + public class MyNpeClass { Integer value = null; From 621d24ce78275ccf6b2d17e7c43264de92b2a87f Mon Sep 17 00:00:00 2001 From: Dmitry Baev Date: Fri, 8 May 2026 13:15:59 +0100 Subject: [PATCH 07/13] Add Allure reporting for Playwright Java (via #1269) --- README.md | 64 +- .../qameta/allure/model/AttachmentType.java | 46 ++ allure-playwright/build.gradle.kts | 36 + .../allure/playwright/AllurePlaywright.java | 571 ++++++++++++++++ .../playwright/AllurePlaywrightAspect.java | 226 ++++++ .../playwright/AllurePlaywrightConfig.java | 97 +++ .../playwright/AllurePlaywrightLifecycle.java | 43 ++ .../playwright/AllurePlaywrightRegistry.java | 128 ++++ .../playwright/DefaultTraceSession.java | 88 +++ .../allure/playwright/PlaywrightAction.java | 285 ++++++++ .../allure/playwright/TraceSession.java | 33 + .../src/main/resources/META-INF/aop-ajc.xml | 6 + ...meta.allure.listener.TestLifecycleListener | 1 + .../playwright/AllurePlaywrightTest.java | 642 ++++++++++++++++++ .../src/test/resources/allure.properties | 3 + settings.gradle.kts | 1 + 16 files changed, 2269 insertions(+), 1 deletion(-) create mode 100644 allure-model/src/main/java/io/qameta/allure/model/AttachmentType.java create mode 100644 allure-playwright/build.gradle.kts create mode 100644 allure-playwright/src/main/java/io/qameta/allure/playwright/AllurePlaywright.java create mode 100644 allure-playwright/src/main/java/io/qameta/allure/playwright/AllurePlaywrightAspect.java create mode 100644 allure-playwright/src/main/java/io/qameta/allure/playwright/AllurePlaywrightConfig.java create mode 100644 allure-playwright/src/main/java/io/qameta/allure/playwright/AllurePlaywrightLifecycle.java create mode 100644 allure-playwright/src/main/java/io/qameta/allure/playwright/AllurePlaywrightRegistry.java create mode 100644 allure-playwright/src/main/java/io/qameta/allure/playwright/DefaultTraceSession.java create mode 100644 allure-playwright/src/main/java/io/qameta/allure/playwright/PlaywrightAction.java create mode 100644 allure-playwright/src/main/java/io/qameta/allure/playwright/TraceSession.java create mode 100644 allure-playwright/src/main/resources/META-INF/aop-ajc.xml create mode 100644 allure-playwright/src/main/resources/META-INF/services/io.qameta.allure.listener.TestLifecycleListener create mode 100644 allure-playwright/src/test/java/io/qameta/allure/playwright/AllurePlaywrightTest.java create mode 100644 allure-playwright/src/test/resources/allure.properties diff --git a/README.md b/README.md index f77b1f03..fd415585 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,69 @@ SelenideLogger.addListener("AllureSelenide", new AllureSelenide().enableLogs(Log https://github.com/SeleniumHQ/selenium/wiki/Logging ``` - +## Playwright Java + +AspectJ-based integration for Playwright Java that reports browser actions as Allure steps and attaches +Playwright screenshots automatically: + +```xml + + io.qameta.allure + allure-playwright + $LATEST_VERSION + +``` + +Enable the AspectJ weaver for automatic action steps: +``` +-javaagent:/path/to/aspectjweaver.jar +``` + +Usage example with Playwright Java JUnit fixtures: +```java +@UsePlaywright +class UiTest { + + @Test + void shouldOpenPage(Page page) { + page.navigate("https://playwright.dev"); + page.screenshot(); + } +} +``` + +The module registers an Allure test lifecycle listener automatically, so per-test cleanup, failure diagnostics, +and final trace/log flush work with any test framework that reports through Allure. Playwright pages and +contexts are tracked by the AspectJ integration when they are created or used. Use +`AllurePlaywright.register(...)` only for pages or contexts the aspect cannot observe. + +Frameworks or custom runners that do not use the Allure lifecycle can call the reporting hooks directly: +```java +AllurePlaywright.beforeTest(); +try { + testBody(); +} catch (Throwable e) { + AllurePlaywright.afterTestFailure(e); + throw e; +} finally { + AllurePlaywright.afterTest(); +} +``` + +The following defaults can be overridden in `allure.properties`: +``` +allure.playwright.steps.enabled=true +allure.playwright.steps.mode=actions +allure.playwright.parameters=redacted +allure.playwright.screenshots.attach=true +allure.playwright.failure.screenshot=true +allure.playwright.failure.page-source=true +allure.playwright.close.trace=true +allure.playwright.close.video=true +allure.playwright.close.page-logs=true +``` + + ## Rest Assured Filter for rest-assured http client, that generates attachment for allure. diff --git a/allure-model/src/main/java/io/qameta/allure/model/AttachmentType.java b/allure-model/src/main/java/io/qameta/allure/model/AttachmentType.java new file mode 100644 index 00000000..806fb391 --- /dev/null +++ b/allure-model/src/main/java/io/qameta/allure/model/AttachmentType.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.model; + +/** + * Common attachment media types and file extensions. + */ +public final class AttachmentType { + + public static final AttachmentType PNG = new AttachmentType("image/png", "png"); + public static final AttachmentType JPEG = new AttachmentType("image/jpeg", "jpg"); + public static final AttachmentType TEXT = new AttachmentType("text/plain", "txt"); + public static final AttachmentType HTML = new AttachmentType("text/html", "html"); + public static final AttachmentType ZIP = new AttachmentType("application/zip", "zip"); + public static final AttachmentType WEBM = new AttachmentType("video/webm", "webm"); + public static final AttachmentType OCTET_STREAM = new AttachmentType("application/octet-stream", ""); + + private final String mediaType; + private final String extension; + + private AttachmentType(final String mediaType, final String extension) { + this.mediaType = mediaType; + this.extension = extension; + } + + public String getMediaType() { + return mediaType; + } + + public String getExtension() { + return extension; + } +} diff --git a/allure-playwright/build.gradle.kts b/allure-playwright/build.gradle.kts new file mode 100644 index 00000000..55bec923 --- /dev/null +++ b/allure-playwright/build.gradle.kts @@ -0,0 +1,36 @@ +description = "Allure Playwright Integration" + +val agent: Configuration by configurations.creating + +val playwrightVersion = "1.59.0" + +dependencies { + agent("org.aspectj:aspectjweaver") + api(project(":allure-java-commons")) + compileOnly("com.microsoft.playwright:playwright:$playwrightVersion") + compileOnly("org.aspectj:aspectjrt") + testAnnotationProcessor(project(":allure-descriptions-javadoc")) + testImplementation("com.microsoft.playwright:playwright:$playwrightVersion") + testImplementation("org.assertj:assertj-core") + testImplementation("org.junit.jupiter:junit-jupiter-api") + testImplementation("org.slf4j:slf4j-simple") + testImplementation(project(":allure-assertj")) + testImplementation(project(":allure-java-commons-test")) + testImplementation(project(":allure-junit-platform")) + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") +} + +tasks.jar { + manifest { + attributes(mapOf( + "Automatic-Module-Name" to "io.qameta.allure.playwright" + )) + } +} + +tasks.test { + useJUnitPlatform() + doFirst { + jvmArgs("-javaagent:${agent.singleFile}") + } +} diff --git a/allure-playwright/src/main/java/io/qameta/allure/playwright/AllurePlaywright.java b/allure-playwright/src/main/java/io/qameta/allure/playwright/AllurePlaywright.java new file mode 100644 index 00000000..ff5a9ad4 --- /dev/null +++ b/allure-playwright/src/main/java/io/qameta/allure/playwright/AllurePlaywright.java @@ -0,0 +1,571 @@ +/* + * 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.playwright; + +import com.microsoft.playwright.Browser; +import com.microsoft.playwright.BrowserContext; +import com.microsoft.playwright.ConsoleMessage; +import com.microsoft.playwright.Page; +import com.microsoft.playwright.Tracing; +import com.microsoft.playwright.Video; +import io.qameta.allure.Allure; +import io.qameta.allure.AllureLifecycle; +import io.qameta.allure.model.AttachmentType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.function.Supplier; + +/** + * Utility methods for attaching Playwright Java diagnostics to the current Allure test. + */ +public final class AllurePlaywright { + + private static final Logger LOGGER = LoggerFactory.getLogger(AllurePlaywright.class); + + private static final String SCREENSHOT = "Screenshot"; + private static final String PAGE_SOURCE = "Page source"; + private static final String TRACE = "Playwright trace"; + private static final String VIDEO = "Playwright video"; + private static final String CONSOLE_MESSAGES = "Console messages"; + private static final String PAGE_ERRORS = "Page errors"; + + private static final ThreadLocal SUPPRESS_ASPECT = new ThreadLocal() { + @Override + protected Boolean initialValue() { + return Boolean.FALSE; + } + }; + + private AllurePlaywright() { + throw new IllegalStateException("Do not instance"); + } + + /** + * Clears Playwright reporting state before a test starts. + */ + public static void beforeTest() { + clear(); + } + + /** + * Attaches final Playwright artifacts and clears reporting state after a test finishes. + */ + public static void afterTest() { + attachRegisteredCloseArtifacts(); + clear(); + } + + /** + * Attaches failure diagnostics for the current test. + */ + public static void afterTestFailure() { + attachFailureArtifacts(); + } + + /** + * Attaches failure diagnostics for the current test. + * + * @param throwable the test failure. + */ + public static void afterTestFailure(final Throwable throwable) { + LOGGER.debug("Attaching Playwright failure artifacts", throwable); + afterTestFailure(); + } + + /** + * Registers a Playwright page for failure diagnostics produced by reporting lifecycle hooks. + * + * @param page the page to register. + */ + public static void register(final Page page) { + AllurePlaywrightRegistry.register(page); + } + + /** + * Registers a Playwright browser context for failure diagnostics produced by reporting lifecycle hooks. + * + * @param context the browser context to register. + */ + public static void register(final BrowserContext context) { + AllurePlaywrightRegistry.register(context); + } + + /** + * Captures and attaches a page screenshot. + * + * @param name the attachment name. + * @param page the page to capture. + */ + public static void attachScreenshot(final String name, final Page page) { + if (page == null || !hasAllureContext()) { + return; + } + try { + final byte[] screenshot = withAspectSuppressed(new Supplier() { + @Override + public byte[] get() { + return page.screenshot(); + } + }); + attachBytes(defaultName(name, SCREENSHOT), AttachmentType.PNG, screenshot); + } catch (RuntimeException e) { + LOGGER.warn("Could not capture Playwright screenshot", e); + } + } + + /** + * Captures and attaches page source. + * + * @param name the attachment name. + * @param page the page to capture. + */ + public static void attachPageSource(final String name, final Page page) { + if (page == null || !hasAllureContext()) { + return; + } + try { + final String content = withAspectSuppressed(new Supplier() { + @Override + public String get() { + return page.content(); + } + }); + attachBytes(defaultName(name, PAGE_SOURCE), AttachmentType.HTML, content.getBytes(StandardCharsets.UTF_8)); + } catch (RuntimeException e) { + LOGGER.warn("Could not capture Playwright page source", e); + } + } + + /** + * Captures and attaches Playwright console messages retained by the page. + * + * @param name the attachment name. + * @param page the page to capture. + */ + public static void attachConsoleMessages(final String name, final Page page) { + if (page == null || !hasAllureContext()) { + return; + } + try { + final String content = formatConsoleMessages(page.consoleMessages()); + attachText(defaultName(name, CONSOLE_MESSAGES), content); + } catch (RuntimeException e) { + LOGGER.warn("Could not capture Playwright console messages", e); + } + } + + /** + * Captures and attaches Playwright page errors retained by the page. + * + * @param name the attachment name. + * @param page the page to capture. + */ + public static void attachPageErrors(final String name, final Page page) { + if (page == null || !hasAllureContext()) { + return; + } + try { + final String content = formatLines(page.pageErrors()); + attachText(defaultName(name, PAGE_ERRORS), content); + } catch (RuntimeException e) { + LOGGER.warn("Could not capture Playwright page errors", e); + } + } + + /** + * Attaches a Playwright trace archive. + * + * @param name the attachment name. + * @param traceZip the path to the trace zip file. + */ + public static void attachTrace(final String name, final Path traceZip) { + attachPath(defaultName(name, TRACE), AttachmentType.ZIP, traceZip); + } + + /** + * Attaches a Playwright video file. + * + * @param name the attachment name. + * @param videoFile the path to the video file. + */ + public static void attachVideo(final String name, final Path videoFile) { + attachPath(defaultName(name, VIDEO), videoType(videoFile), videoFile); + } + + /** + * Starts Playwright tracing and registers the trace for failure diagnostics. + * + * @param context the browser context to trace. + * @return trace session that stops tracing and attaches the generated archive when closed. + */ + public static TraceSession startTracing(final BrowserContext context) { + return startTracing(TRACE, context); + } + + /** + * Starts Playwright tracing and registers the trace for failure diagnostics. + * + * @param name the attachment name to use when the trace is attached. + * @param context the browser context to trace. + * @return trace session that stops tracing and attaches the generated archive when closed. + */ + public static TraceSession startTracing(final String name, final BrowserContext context) { + if (context == null) { + throw new IllegalArgumentException("context must not be null"); + } + final Tracing.StartOptions options = new Tracing.StartOptions() + .setScreenshots(true) + .setSnapshots(true); + context.tracing().start(options); + final DefaultTraceSession traceSession = new DefaultTraceSession(context, defaultName(name, TRACE)); + AllurePlaywrightRegistry.register(context); + AllurePlaywrightRegistry.register(traceSession); + return traceSession; + } + + static CloseArtifacts beforeClose(final Object target) { + final CloseArtifacts closeArtifacts = new CloseArtifacts(); + if (target instanceof Page) { + collectPageCloseArtifacts((Page) target, closeArtifacts); + } else if (target instanceof BrowserContext) { + collectContextCloseArtifacts((BrowserContext) target, closeArtifacts); + } else if (target instanceof Browser) { + collectBrowserCloseArtifacts((Browser) target, closeArtifacts); + } + return closeArtifacts; + } + + static void attachScreenshotBytes(final String name, final AttachmentType type, final byte[] bytes) { + if (!AllurePlaywrightConfig.shouldAttachScreenshots()) { + return; + } + attachBytes(defaultName(name, SCREENSHOT), type, bytes); + } + + static AttachmentType screenshotType(final Object... args) { + final Object options = args.length == 0 ? null : args[0]; + if (isJpegType(options)) { + return AttachmentType.JPEG; + } + if (isJpegPath(options)) { + return AttachmentType.JPEG; + } + return AttachmentType.PNG; + } + + static boolean hasAllureContext() { + return Allure.getLifecycle().getCurrentTestCaseOrStep().isPresent(); + } + + static boolean isAspectSuppressed() { + return SUPPRESS_ASPECT.get(); + } + + static T withAspectSuppressed(final Supplier supplier) { + final Boolean previous = SUPPRESS_ASPECT.get(); + SUPPRESS_ASPECT.set(Boolean.TRUE); + try { + return supplier.get(); + } finally { + SUPPRESS_ASPECT.set(previous); + } + } + + @SuppressWarnings("PMD.CloseResource") + static void clear() { + for (DefaultTraceSession traceSession : AllurePlaywrightRegistry.getTraceSessions()) { + traceSession.stopWithoutAttachment(); + } + AllurePlaywrightRegistry.clear(); + } + + static void attachFailureArtifacts() { + if (!AllurePlaywrightRegistry.markFailureArtifactsAttached()) { + return; + } + if (AllurePlaywrightConfig.shouldAttachFailureScreenshot()) { + attachFailureScreenshots(); + } + if (AllurePlaywrightConfig.shouldAttachFailurePageSource()) { + attachFailurePageSources(); + } + attachFailureTraces(); + } + + @SuppressWarnings("PMD.CloseResource") + static void attachRegisteredCloseArtifacts() { + if (!hasAllureContext()) { + return; + } + if (AllurePlaywrightConfig.shouldAttachClosePageLogs()) { + for (Page page : AllurePlaywrightRegistry.getPages()) { + attachPageCloseLogs(page); + } + } + if (AllurePlaywrightConfig.shouldAttachCloseTrace()) { + attachFailureTraces(); + } + } + + @SuppressWarnings("PMD.CloseResource") + private static void collectBrowserCloseArtifacts(final Browser browser, final CloseArtifacts closeArtifacts) { + for (BrowserContext context : contexts(browser)) { + collectContextCloseArtifacts(context, closeArtifacts); + } + } + + @SuppressWarnings("PMD.CloseResource") + private static void collectContextCloseArtifacts(final BrowserContext context, + final CloseArtifacts closeArtifacts) { + register(context); + for (Page page : pages(context)) { + collectPageCloseArtifacts(page, closeArtifacts); + } + if (AllurePlaywrightConfig.shouldAttachCloseTrace()) { + attachCloseTraces(context); + } + } + + private static void collectPageCloseArtifacts(final Page page, final CloseArtifacts closeArtifacts) { + register(page); + attachPageCloseLogs(page); + if (AllurePlaywrightRegistry.markCloseVideoAttached(page)) { + closeArtifacts.addVideo(video(page)); + } + } + + private static void attachPageCloseLogs(final Page page) { + if (AllurePlaywrightConfig.shouldAttachClosePageLogs() + && AllurePlaywrightRegistry.markClosePageLogsAttached(page)) { + attachConsoleMessages(CONSOLE_MESSAGES, page); + attachPageErrors(PAGE_ERRORS, page); + } + } + + @SuppressWarnings("PMD.CloseResource") + private static void attachCloseTraces(final BrowserContext context) { + for (DefaultTraceSession traceSession : AllurePlaywrightRegistry.getTraceSessions(context)) { + traceSession.attach(); + } + } + + @SuppressWarnings("PMD.CloseResource") + private static void attachFailureScreenshots() { + for (Page page : AllurePlaywrightRegistry.getPages()) { + attachScreenshot(SCREENSHOT, page); + } + } + + @SuppressWarnings("PMD.CloseResource") + private static void attachFailurePageSources() { + for (Page page : AllurePlaywrightRegistry.getPages()) { + attachPageSource(PAGE_SOURCE, page); + } + } + + @SuppressWarnings("PMD.CloseResource") + private static void attachFailureTraces() { + for (DefaultTraceSession traceSession : AllurePlaywrightRegistry.getTraceSessions()) { + traceSession.attach(); + } + } + + private static List contexts(final Browser browser) { + try { + return browser.contexts(); + } catch (RuntimeException e) { + LOGGER.warn("Could not collect Playwright browser contexts", e); + return Collections.emptyList(); + } + } + + private static List pages(final BrowserContext context) { + try { + return context.pages(); + } catch (RuntimeException e) { + LOGGER.warn("Could not collect Playwright pages", e); + return Collections.emptyList(); + } + } + + private static Video video(final Page page) { + try { + return page.video(); + } catch (RuntimeException e) { + LOGGER.warn("Could not collect Playwright video", e); + return null; + } + } + + private static AttachmentType videoType(final Path path) { + if (path == null) { + return AttachmentType.WEBM; + } + final String name = path.getFileName().toString().toLowerCase(Locale.ROOT); + if (name.endsWith(".webm")) { + return AttachmentType.WEBM; + } + return AttachmentType.OCTET_STREAM; + } + + private static void attachPath(final String name, final AttachmentType type, final Path path) { + if (path == null || !hasAllureContext() || !Files.isRegularFile(path)) { + return; + } + try { + attachBytes(name, type, Files.readAllBytes(path)); + } catch (IOException e) { + LOGGER.warn("Could not attach Playwright artifact {}", path, e); + } + } + + private static void attachBytes(final String name, final AttachmentType type, final byte[] bytes) { + if (bytes == null || !hasAllureContext()) { + return; + } + final AllureLifecycle lifecycle = Allure.getLifecycle(); + lifecycle.addAttachment(name, type.getMediaType(), type.getExtension(), bytes); + } + + private static void attachText(final String name, final String content) { + if (content == null || content.isEmpty()) { + return; + } + attachBytes(name, AttachmentType.TEXT, content.getBytes(StandardCharsets.UTF_8)); + } + + private static String defaultName(final String name, final String fallback) { + return name == null || name.isEmpty() ? fallback : name; + } + + private static boolean isJpegType(final Object options) { + final Object type = readField(options, "type"); + return type != null && "JPEG".equals(type.toString()); + } + + private static boolean isJpegPath(final Object options) { + final Object path = readField(options, "path"); + if (!(path instanceof Path)) { + return false; + } + final String name = ((Path) path).getFileName().toString().toLowerCase(Locale.ROOT); + return name.endsWith(".jpg") || name.endsWith(".jpeg"); + } + + private static Object readField(final Object target, final String name) { + if (target == null) { + return null; + } + try { + final Field field = target.getClass().getField(name); + return field.get(target); + } catch (ReflectiveOperationException ignored) { + return null; + } + } + + private static String formatConsoleMessages(final List messages) { + if (messages == null || messages.isEmpty()) { + return ""; + } + final StringBuilder builder = new StringBuilder(); + for (ConsoleMessage message : messages) { + final String type = safe(new Supplier() { + @Override + public String get() { + return message.type(); + } + }); + final String text = safe(new Supplier() { + @Override + public String get() { + return message.text(); + } + }); + final String location = safe(new Supplier() { + @Override + public String get() { + return message.location(); + } + }); + builder.append('[').append(type).append("] ").append(text); + if (!location.isEmpty()) { + builder.append(" (").append(location).append(')'); + } + builder.append(System.lineSeparator()); + } + return builder.toString(); + } + + private static String formatLines(final List lines) { + if (lines == null || lines.isEmpty()) { + return ""; + } + final StringBuilder builder = new StringBuilder(); + for (String line : lines) { + builder.append(line).append(System.lineSeparator()); + } + return builder.toString(); + } + + private static String safe(final Supplier supplier) { + try { + final String value = supplier.get(); + return value == null ? "" : value; + } catch (RuntimeException ignored) { + return ""; + } + } + + static final class CloseArtifacts { + + private final List