diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8f537bc9c..341788f5e 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 000000000..8b7a6aa4d --- /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 9c0b0a2d7..c881472e6 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 9c0b0a2d7..b47a01f60 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-citrus/src/test/resources/allure.properties b/allure-citrus/src/test/resources/allure.properties index 9c0b0a2d7..0833b8e02 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 dbfefee44..e026a61f0 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 dbfefee44..8e2960725 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 dbfefee44..6b16969f8 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 dbfefee44..20fde5c18 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 9c0b0a2d7..384d9cd50 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 9c0b0a2d7..556029437 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-httpclient/src/test/resources/allure.properties b/allure-httpclient/src/test/resources/allure.properties index 9c0b0a2d7..0b9f016cb 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 9c0b0a2d7..a6feadd77 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 e1d5c3995..92c4e2e00 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 000000000..4f5048a55 --- /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 000000000..607d7013f --- /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 000000000..12bd9c5d5 --- /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 000000000..d7fb298d1 --- /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/src/test/resources/allure.properties b/allure-java-commons/src/test/resources/allure.properties index e4f1d9fc1..9f941c45f 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 9c0b0a2d7..cd2d7056f 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 9c0b0a2d7..04f5f5b67 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 9c0b0a2d7..d61e9c1d7 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-jsonunit/src/test/resources/allure.properties b/allure-jsonunit/src/test/resources/allure.properties index 9c0b0a2d7..5f86fe638 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 4cf244bf2..b090e9ec6 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 9c0b0a2d7..cff9d0857 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 79d3bc537..df7435024 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 000000000..d76ba2075 --- /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/src/test/resources/allure.properties b/allure-junit4/src/test/resources/allure.properties index 9c0b0a2d7..b68758d17 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 9c0b0a2d7..2f82d5faa 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 4c9c3b8ee..36ea007c4 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-model/src/test/resources/allure.properties b/allure-model/src/test/resources/allure.properties index cb77d0a3e..5002f01c0 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 9c0b0a2d7..2182ac90e 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 9c0b0a2d7..a835d79ec 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 432fd8158..6d05533e7 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 000000000..74802ce12 --- /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-rest-assured/src/test/resources/allure.properties b/allure-rest-assured/src/test/resources/allure.properties index 9c0b0a2d7..89b92e231 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 9c0b0a2d7..760733e93 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 9c0b0a2d7..cfcad4a8e 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 55c4bdcdf..52c92821b 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 fe1c3abc4..314667b14 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 000000000..4fca9bfc7 --- /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-spock/src/test/resources/allure.properties b/allure-spock/src/test/resources/allure.properties index 9c0b0a2d7..92d17a33c 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 9c0b0a2d7..19503db35 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 9c0b0a2d7..594c44e14 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 b2198ce39..2c96987f2 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 000000000..dda3d2de3 --- /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 000000000..b66cf67c5 --- /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-testng/build.gradle.kts b/allure-testng/build.gradle.kts index 7d6fe56a7..7113509d6 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 bc1f54bf0..498befd58 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 e4f1d9fc1..69cd85a70 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 000000000..0e718deea --- /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/allurerc.yml b/allurerc.yml new file mode 100644 index 000000000..ac7b02172 --- /dev/null +++ b/allurerc.yml @@ -0,0 +1,2 @@ +name: Allure Java +output: ./build/allure-report diff --git a/build.gradle.kts b/build.gradle.kts index ac086dec1..962913572 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 000000000..f99482013 --- /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 a59520664..e48eca575 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 ad6f5e51d..02cf649af 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"