diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 5fc5ee427..000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,47 +0,0 @@ -[//]: # ( -. Note: for support questions, please use Stackoverflow or Gitter**. -. This repository's issues are reserved for feature requests and bug reports. -. -. In case of any problems with Allure Jenkins plugin** please use the following repository -. to create an issue: https://github.com/jenkinsci/allure-plugin/issues -. -. Make sure you have a clear name for your issue. The name should start with a capital -. letter and no dot is required in the end of the sentence. An example of good issue names: -. -. - The report is broken in IE11 -. - Add an ability to disable default plugins -. - Support emoji in test descriptions -) - -#### I'm submitting a ... - - [ ] bug report - - [ ] feature request - - [ ] support request => Please do not submit support request here, see note at the top of this template. - -#### What is the current behavior? - - -#### If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem - - -#### What is the expected behavior? - - -#### What is the motivation / use case for changing the behavior? - - -#### Please tell us about your environment: - - -| Allure version | 2.2.0 | -| --- | --- | -| Test framework | testng@6.8 | -| Allure integration | allure-testng@2.0-BETA11 | -| Generate report using | allure-maven@2.18 | - -#### Other information - -[//]: # ( -. e.g. detailed explanation, stacktraces, related issues, suggestions -. how to fix, links for us to have more context, eg. Stackoverflow, Gitter etc -) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 319290ead..363b5c7ef 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,7 +1,6 @@ -name: 🐞 Bug Report -description: File a Bug report in Java Integration -title: "🐞: " -labels: [ "type:bug", "triage" ] +name: Bug report +description: Report broken or incorrect behavior in an Allure Java integration. +type: bug assignees: [] body: - type: markdown @@ -25,42 +24,40 @@ body: description: Please select the Allure integration you options: - allure-assertj - - allure-attachments - allure-awaitility + - allure-bom - allure-citrus - - allure-cucumber2-jvm - - allure-cucumber3-jvm - - allure-cucumber4-jvm - - allure-cucumber5-jvm - - allure-cucumber6-jvm - allure-cucumber7-jvm - allure-descriptions-javadoc - allure-grpc - allure-hamcrest - allure-httpclient + - allure-httpclient5 + - allure-java-httpclient - allure-java-commons + - allure-java-commons-test - allure-jax-rs - - allure-jbehave - allure-jbehave5 + - allure-jooq - allure-jsonunit - allure-junit-platform - allure-junit4 + - allure-junit4-aspect - allure-jupiter - allure-jupiter-assert - - allure-junit5 - - allure-junit5-assert - allure-karate - - allure-okhttp + - allure-kotlin-coroutines + - allure-kotlin-extensions + - allure-model - allure-okhttp3 - - allure-reader + - allure-playwright - allure-rest-assured - allure-scalatest + - allure-selenium-bidi - allure-selenide - allure-servlet-api - - allure-spock - allure-spock2 - allure-spring-web - - allure-test-filter - allure-testng validations: required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..eb189d888 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,32 @@ +name: Feature request +description: Suggest a user-facing feature or improvement. +type: feature +body: + - type: markdown + attributes: + value: | + Thanks for suggesting an improvement for Allure Java. Please describe the outcome you need, not only a possible implementation. + - type: textarea + id: problem + attributes: + label: What problem should this solve? + description: Describe the workflow, limitation, or user need behind the request. + validations: + required: true + - type: textarea + id: proposal + attributes: + label: What would you like to happen? + description: Describe the behavior or capability you want Allure Java to provide. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Share any current workaround or alternative design you have considered. + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Add examples, links, screenshots, or related issues. diff --git a/.github/ISSUE_TEMPLATE/task.yml b/.github/ISSUE_TEMPLATE/task.yml new file mode 100644 index 000000000..7cd649f04 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.yml @@ -0,0 +1,16 @@ +name: Task +description: Track internal maintenance or repository work. +type: task +body: + - type: textarea + id: goal + attributes: + label: Goal + description: Describe the maintenance, cleanup, or repository task. + validations: + required: true + - type: textarea + id: details + attributes: + label: Details + description: Add relevant context, constraints, links, or acceptance criteria. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 240d09840..70c93749e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,11 +5,11 @@ updates: schedule: interval: "daily" labels: - - "type:dependencies" + - "pr:dependencies" - package-ecosystem: "gradle" directory: "/" schedule: interval: "daily" labels: - - "type:dependencies" + - "pr:dependencies" diff --git a/.github/labeler.yml b/.github/labeler.yml deleted file mode 100644 index 367271a00..000000000 --- a/.github/labeler.yml +++ /dev/null @@ -1,84 +0,0 @@ -"theme:workflow": - - ".github/**" - -"theme:build": - - "gradle/**" - - "build.gradle.kts" - - "**/build.gradle.kts" - - "gradle.properties" - - ".gitignore" - -"theme:assertj": - - "allure-assertj/**" - -"theme:attachments": - - "allure-attachments/**" - -"theme:citrus": - - "allure-citrus/**" - -"theme:cucumber-jvm": - - "allure-cucumber*-jvm/**" - -"theme:descriptions-javadoc": - - "allure-descriptions-javadoc/**" - -"theme:httpclient": - - "allure-httpclient/**" - -"theme:model": - - "allure-model/**" - -"theme:core": - - "allure-java-commons/**" - - "allure-java-commons-test/**" - - "allure-test-filter/**" - -"theme:jax-rs": - - "allure-jax-rs/**" - -"theme:jbehave": - - "allure-jbehave*/**" - -"theme:jsonunit": - - "allure-jsonunit/**" - -"theme:junit4": - - "allure-junit4/**" - - "allure-junit4-aspect/**" - -"theme:junit-platform": - - "allure-jupiter/**" - - "allure-jupiter-assert/**" - - "allure-junit-platform/**" - -"theme:karate": - - "allure-karate/**" - -"theme:okhttp": - - "allure-okhttp/**" - - "allure-okhttp3/**" - -"theme:rest-assured": - - "allure-rest-assured/**" - -"theme:scalatest": - - "allure-scalatest/**" - -"theme:selenide": - - "allure-selenide/**" - -"theme:servlet-api": - - "allure-servlet-api/**" - -"theme:spock": - - "allure-spock/**" - -"theme:spring": - - "allure-spring-web/**" - -"theme:testng": - - "allure-testng/**" - -"theme:hamcrest": - - "allure-hamcrest/**" diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/pull_request_template.md similarity index 74% rename from .github/PULL_REQUEST_TEMPLATE.md rename to .github/pull_request_template.md index 8178c9b61..4ccdd31e5 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/pull_request_template.md @@ -1,7 +1,7 @@ ### Context + #### Checklist + - [ ] [Sign Allure CLA][cla] - [ ] Provide unit tests -[cla]: https://cla-assistant.io/accept/allure-framework/allure2 +[cla]: https://cla-assistant.io/accept/allure-framework/allure-java diff --git a/.github/release.yml b/.github/release.yml index 7c30a5979..7670c3878 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -1,25 +1,28 @@ # release.yml changelog: + exclude: + labels: + - 'pr:invalid' categories: - title: '🚀 New Features' labels: - - 'type:new feature' + - 'pr:new feature' - title: '🔬 Improvements' labels: - - 'type:improvement' + - 'pr:improvement' - title: '🐞 Bug Fixes' labels: - - 'type:bug' + - 'pr:bug' - title: '⬆️ Dependency Updates' labels: - - 'type:dependencies' + - 'pr:dependencies' - title: '📖 Documentation improvements' labels: - - 'type:documentation' + - 'pr:documentation' - title: '⛔️ Security' labels: - - 'type:security' + - 'pr:security' - title: '👻 Internal changes' labels: - - 'type:internal' + - 'pr:internal' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 341788f5e..d0e9e7f0e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,35 +21,54 @@ concurrency: jobs: build: - name: "Build" + name: "Build (JDK ${{ matrix.java }})" runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + java: [17, 25] env: - ALLURE_MATRIX_ENV: ubuntu-jdk-21 - ALLURE_TEST_DUMP_NAME: allure-results-test-jdk-21 + ALLURE_MATRIX_ENV: ubuntu-jdk-${{ matrix.java }} + ALLURE_TEST_DUMP_NAME: allure-results-test-jdk-${{ matrix.java }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7 with: node-version: '20.x' - name: "Set up JDK" - uses: actions/setup-java@v5 + uses: actions/setup-java@v6 with: distribution: 'zulu' - java-version: 21 + java-version: ${{ matrix.java }} - name: "Setup Gradle" uses: gradle/actions/setup-gradle@v6 with: gradle-version: 'wrapper' + - name: "Select Gradle task exclusions" + shell: bash + run: | + if [[ "${{ matrix.java }}" == "17" ]]; then + higher_java_modules=( + ":allure-jooq" + ":allure-karate" + ) + excludes=() + for module in "${higher_java_modules[@]}"; do + excludes+=("-x" "$module:build" "-x" "$module:test") + done + echo "GRADLE_EXCLUDES=${excludes[*]}" >> "$GITHUB_ENV" + fi + - name: "Build with Gradle" - run: ./gradlew build -x test --scan + run: ./gradlew build -x test $GRADLE_EXCLUDES --scan - name: "Run tests with Allure" if: always() - 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 + 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 $GRADLE_EXCLUDES - name: "Upload Allure test dump" if: always() @@ -69,10 +88,13 @@ jobs: checks: write env: ALLURE_SERVICE_TOKEN: ${{ secrets.ALLURE_SERVICE_TOKEN }} + ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} + ALLURE_ENDPOINT: ${{ secrets.ALLURE_ENDPOINT }} + ALLURE_PROJECT_ID: ${{ secrets.ALLURE_PROJECT_ID }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7 with: node-version: '20.x' diff --git a/.github/workflows/dependency-submission.yml b/.github/workflows/dependency-submission.yml index d02a1dbee..31faaec6e 100644 --- a/.github/workflows/dependency-submission.yml +++ b/.github/workflows/dependency-submission.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Generate and submit dependency graph uses: gradle/actions/dependency-submission@v6 env: diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml deleted file mode 100644 index f49976b59..000000000 --- a/.github/workflows/labeler.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: "Set theme labels" - -on: - - pull_request_target - -permissions: - contents: read - -jobs: - triage: - runs-on: ubuntu-latest - permissions: - pull-requests: write - steps: - - uses: actions/labeler@v4 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/labels-verify.yml b/.github/workflows/labels-verify.yml index 7315a905a..c94a1e441 100644 --- a/.github/workflows/labels-verify.yml +++ b/.github/workflows/labels-verify.yml @@ -1,27 +1,61 @@ -name: "Verify type labels" +name: "Verify PR labels" on: - pull_request_target: - types: [opened, labeled, unlabeled, synchronize] + pull_request: + types: + - opened + - reopened + - synchronize + - ready_for_review + - labeled + - unlabeled + - edited permissions: - contents: none + contents: read + pull-requests: read + issues: read jobs: triage: + name: Require exactly one pr label runs-on: ubuntu-latest - permissions: - pull-requests: read steps: - - uses: baev/action-label-verify@main - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - allowed: | - type:bug - type:dependencies - type:improvement - type:internal - type:invalid - type:new feature - type:security - type:documentation + - name: Validate release notes label + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + shell: bash + run: | + labels="$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/labels" --jq '.[].name')" + pr_labels="$(printf '%s\n' "${labels}" | grep '^pr:' || true)" + + if [ -z "${pr_labels}" ]; then + count=0 + else + count="$(printf '%s\n' "${pr_labels}" | wc -l | tr -d ' ')" + fi + + if [ "${count}" -eq 1 ]; then + echo "Found PR release-note label: ${pr_labels}" + exit 0 + fi + + { + echo "Pull requests must have exactly one label with the pr: prefix." + echo "Found ${count} pr: labels." + if [ "${count}" -gt 0 ]; then + printf 'Matching labels:\n' + printf '%s\n' "${pr_labels}" | sed 's/^/- /' + fi + echo + echo "Available labels on this PR:" + if [ -n "${labels}" ]; then + printf '%s\n' "${labels}" | sed 's/^/- /' + else + echo "- " + fi + } >> "${GITHUB_STEP_SUMMARY}" + + echo "::error title=Invalid pr: label count::Pull requests must have exactly one label with the pr: prefix. Found ${count}." + exit 1 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fd0a661c4..bc4eb73dc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,10 +11,10 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: "Set up JDK" - uses: actions/setup-java@v5 + uses: actions/setup-java@v6 with: distribution: 'zulu' java-version: '21' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7d5ace556..1af69ff51 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,13 +12,14 @@ on: required: true permissions: - contents: read + contents: read jobs: triage: runs-on: ubuntu-latest - permissions: - contents: write + env: + GIT_COMMIT_AUTHOR_EMAIL: ${{ vars.ALLURE_CI_APP_USER_ID }}+${{ vars.ALLURE_CI_APP_SLUG }}[bot]@users.noreply.github.com + GIT_COMMIT_AUTHOR_NAME: ${{ vars.ALLURE_CI_APP_SLUG }}[bot] steps: - name: "Check release version" run: | @@ -26,14 +27,23 @@ jobs: - name: "Check next version" run: | expr "${{ github.event.inputs.nextVersion }}" : '[[:digit:]][[:digit:]]*\.[[:digit:]][[:digit:]]*$' - - uses: actions/checkout@v6 + + - name: Create GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.ALLURE_CI_APP_CLIENT_ID }} + private-key: ${{ secrets.ALLURE_CI_APP_PRIVATE_KEY }} + permission-contents: write + + - uses: actions/checkout@v7 with: - token: ${{ secrets.QAMETA_CI }} + token: ${{ steps.app-token.outputs.token }} - name: "Configure CI Git User" run: | - git config --global user.name qameta-ci - git config --global user.email qameta-ci@qameta.io + git config --global user.name "${GIT_COMMIT_AUTHOR_NAME}" + git config --global user.email "${GIT_COMMIT_AUTHOR_EMAIL}" - name: "Set release version" run: | sed -i -e '/version=/s/.*/version=${{ github.event.inputs.releaseVersion }}/g' gradle.properties @@ -59,4 +69,4 @@ jobs: generate_release_notes: true target_commitish: ${{ github.ref }} env: - GITHUB_TOKEN: ${{ secrets.QAMETA_CI }} + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/.gitignore b/.gitignore index b85107a90..fc9f1bb0d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ build out .gradletasknamecache +#AspectJ crash dumps +ajcore.*.txt + #IDEA Files .idea/* !.idea/vcs.xml diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 95443a122..d154cb226 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -10,4 +10,7 @@ + + + \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 8b7a6aa4d..732e8f7b1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,8 +1,24 @@ # Project Guide -Use [Allure Agent Mode](docs/allure-agent-mode.md) for all test-related work in this repository. +Never create pull requests or push git branches without explicit confirmation from the user. + +## Test Work + +Use [Allure Agent Mode](docs/allure-agent-mode.md) for 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. +- Use the `$allure-agent-mode` skill as the durable behavior guide when it is installed; this project file contains local commands and conventions. +- If a command executes tests and its result will be used for smoke checking, reasoning, review, coverage analysis, debugging, or a user-facing conclusion, run it through `allure agent`. +- Use agent-mode execution for smoke checks too, even when the change is small or mechanical. +- If agent output is missing or incomplete, debug that first and treat console-only conclusions as provisional. + +## Validation + +After making changes, run the applicable validation checks before reporting done. + +- For Java, Groovy, or Scala production/test changes, run the relevant module-scoped quality checks when practical: `::spotlessCheck`, `::checkstyleMain`, `::pmdMain`, and `::spotbugsMain`. +- For shared test-support, build logic, root configuration, or broad cross-module changes, run the aggregate quality command: `./gradlew --no-daemon spotlessCheck checkstyleMain pmdMain spotbugsMain`. +- If Spotless reports formatting issues, run the matching `spotlessApply` task, such as `::spotlessApply` or `./gradlew --no-daemon spotlessApply`, then rerun `spotlessCheck`. +- If a quality check fails, fix the smallest relevant issue and rerun the failed check or the original quality command until it passes. +- For docs-only changes, run `git diff --check` and any configured documentation lint before reporting completion. +- If validation cannot be run, state exactly which checks were skipped and why. diff --git a/README.md b/README.md index 2c6f44a12..d28aa37b7 100644 --- a/README.md +++ b/README.md @@ -1,341 +1,125 @@ -[license]: http://www.apache.org/licenses/LICENSE-2.0 "Apache License 2.0" -[blog]: https://qameta.io/blog -[gitter]: https://gitter.im/allure-framework/allure-core -[gitter-ru]: https://gitter.im/allure-framework/allure-ru -[twitter]: https://twitter.com/QametaSoftware "Qameta Software" -[twitter-team]: https://twitter.com/QametaSoftware/lists/team/members "Team" - -[CONTRIBUTING.md]: .github/CONTRIBUTING.md -[docs]: https://allurereport.org/docs/ - -# Allure Java Integrations - -[![Build](https://github.com/allure-framework/allure-java/actions/workflows/build.yml/badge.svg)](https://github.com/allure-framework/allure-java/actions/workflows/build.yml) -[![Allure Java](https://img.shields.io/github/release/allure-framework/allure-java.svg)](https://github.com/allure-framework/allure-java/releases/latest) - -> The repository contains new versions of adaptors for JVM-based test frameworks. - -[Allure Report logo](https://allurereport.org "Allure Report") - -- Learn more about Allure Report at [https://allurereport.org](https://allurereport.org) -- 📚 [Documentation](https://allurereport.org/docs/) – discover official documentation for Allure Report -- ❓ [Questions and Support](https://github.com/orgs/allure-framework/discussions/categories/questions-support) – get help from the team and community -- 📢 [Official announcements](https://github.com/orgs/allure-framework/discussions/categories/announcements) – stay updated with our latest news and updates -- 💬 [General Discussion](https://github.com/orgs/allure-framework/discussions/categories/general-discussion) – engage in casual conversations, share insights and ideas with the community -- 🖥️ [Live Demo](https://demo.allurereport.org/) — explore a live example of Allure Report in action - ---- -## TestNG - -- 🚀 Documentation — https://allurereport.org/docs/testng/ -- 📚 Example project — https://github.com/allure-examples?q=topic%3Atestng -- ✅ Generate a project in 10 seconds via Allure Start - https://allurereport.org/start/ - -## JUnit 4 - -- 🚀 Documentation — work in progress -- 📚 Example project — https://github.com/allure-examples?q=topic%3Ajunit4 -- ✅ Generate a project in 10 seconds via Allure Start - https://allurereport.org/start/ -- -## JUnit Jupiter (JUnit 5 and 6) +# Allure Java -- 🚀 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 relocation coordinate during migration. - -## Cucumber JVM - -- 🚀 Documentation — https://allurereport.org/docs/cucumberjvm/ -- 📚 Example project — https://github.com/allure-examples?q=cucumber&type=all&language=java -- ✅ Generate a project in 10 seconds via Allure Start - https://allurereport.org/start/ - -## Spock - -- 🚀 Documentation — https://allurereport.org/docs/spock/ -- 📚 Example project — https://github.com/allure-examples?q=topic%3Aspock -- ✅ Generate a project in 10 seconds via Allure Start - https://allurereport.org/start/ - -## Selenide - -Listener for Selenide, that logging steps for Allure: - -```xml - - io.qameta.allure - allure-selenide - $LATEST_VERSION - -``` - -Usage example: -``` -SelenideLogger.addListener("AllureSelenide", new AllureSelenide().screenshots(true).savePageSource(false)); - -Capture selenium logs: -SelenideLogger.addListener("AllureSelenide", new AllureSelenide().enableLogs(LogType.BROWSER, Level.ALL)); -https://github.com/SeleniumHQ/selenium/wiki/Logging -``` - -## Playwright Java +Allure Java is the JVM integration family for [Allure Report](https://allurereport.org/). It helps Java, Groovy, Scala, Kotlin, and other JVM test suites write Allure result files that can be viewed in Allure Report. -AspectJ-based integration for Playwright Java that reports browser actions as Allure steps and attaches -Playwright screenshots automatically: +## Requirements -```xml - - io.qameta.allure - allure-playwright - $LATEST_VERSION - -``` +- Allure Java 3.x targets Java 17 and newer. +- Use one framework adapter per test runtime, for example `allure-jupiter`, `allure-testng`, or `allure-cucumber7-jvm`. +- Prefer `allure-bom` to keep Allure module versions aligned. -Enable the AspectJ weaver for automatic action steps: -``` --javaagent:/path/to/aspectjweaver.jar -``` +## Quick Start -Usage example with Playwright Java JUnit fixtures: -```java -@UsePlaywright -class UiTest { +Gradle: - @Test - void shouldOpenPage(Page page) { - page.navigate("https://playwright.dev"); - page.screenshot(); - } +```kotlin +dependencies { + testImplementation(platform("io.qameta.allure:allure-bom:")) + testImplementation("io.qameta.allure:allure-jupiter") } -``` -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(); +tasks.test { + useJUnitPlatform() } ``` -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. - -```xml - - io.qameta.allure - allure-rest-assured - $LATEST_VERSION - -``` - -Usage example: -``` -.filter(new AllureRestAssured()) -``` -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")) -``` - -## Spring Web - -Interceptor for Spring synchronous HTTP clients, that generates attachments for allure. +Maven: ```xml - - io.qameta.allure - allure-spring-web - $LATEST_VERSION - + + + + io.qameta.allure + allure-bom + ${allure.version} + pom + import + + + + + + + io.qameta.allure + allure-jupiter + test + + ``` -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. +Run your tests, then generate or serve the report from the produced Allure results directory, usually `build/allure-results` for Gradle or `target/allure-results` for Maven. + +## Module Catalog + +### Test Frameworks + +| Module | Use When | Registration | +| --- | --- | --- | +| [`allure-jupiter`](allure-jupiter/README.md) | JUnit Jupiter on JUnit 5 or 6 | JUnit Platform service loader | +| [`allure-junit-platform`](allure-junit-platform/README.md) | Building a custom JUnit Platform integration | JUnit Platform listener and filter services | +| [`allure-junit4`](allure-junit4/README.md) | JUnit 4 runner where listeners can be configured | Register `io.qameta.allure.junit4.AllureJunit4` | +| [`allure-junit4-aspect`](allure-junit4-aspect/README.md) | Gradle built-in JUnit 4 execution | Enable AspectJ weaver | +| [`allure-testng`](allure-testng/README.md) | TestNG 7 suites | TestNG service loader or listener registration | +| [`allure-spock2`](allure-spock2/README.md) | Spock 2 specifications | Spock global extension service | +| [`allure-scalatest`](allure-scalatest/README.md) | ScalaTest suites | ScalaTest reporter | +| [`allure-cucumber7-jvm`](allure-cucumber7-jvm/README.md) | Cucumber JVM 7 | Cucumber plugin | +| [`allure-jbehave5`](allure-jbehave5/README.md) | JBehave 5 stories | JBehave story reporter | +| [`allure-karate`](allure-karate/README.md) | Karate runtime listeners | Karate runtime listener | +| [`allure-citrus`](allure-citrus/README.md) | Citrus tests | Citrus listeners | + +### HTTP, Browser, And Client Integrations + +| Module | Use When | Captured Data | +| --- | --- | --- | +| [`allure-rest-assured`](allure-rest-assured/README.md) | REST Assured filters | HTTP requests and responses | +| [`allure-java-httpclient`](allure-java-httpclient/README.md) | Java built-in `HttpClient` wrapper | HTTP requests and responses | +| [`allure-httpclient5`](allure-httpclient5/README.md) | Apache HttpClient 5 interceptors | HTTP requests and responses | +| [`allure-httpclient`](allure-httpclient/README.md) | Apache HttpClient 4 interceptors | HTTP requests and responses | +| [`allure-okhttp3`](allure-okhttp3/README.md) | OkHttp interceptors | HTTP requests and responses | +| [`allure-spring-web`](allure-spring-web/README.md) | Spring `RestTemplate` interceptors | HTTP requests and responses | +| [`allure-jax-rs`](allure-jax-rs/README.md) | Jakarta RESTful Web Services / JAX-RS client filters | HTTP requests and responses | +| [`allure-servlet-api`](allure-servlet-api/README.md) | Jakarta Servlet request/response conversion | Servlet request and response data | +| [`allure-grpc`](allure-grpc/README.md) | gRPC client interceptors | gRPC calls and metadata | +| [`allure-selenide`](allure-selenide/README.md) | Selenide UI tests | UI steps, screenshots, page source, logs | +| [`allure-selenium-bidi`](allure-selenium-bidi/README.md) | Selenium WebDriver BiDi sessions | Browser logs and network attachments | +| [`allure-playwright`](allure-playwright/README.md) | Playwright Java actions | AspectJ action steps and screenshots | +| [`allure-jooq`](allure-jooq/README.md) | jOOQ execution listener | SQL execution steps | + +### Assertions And Utilities + +| Module | Use When | Notes | +| --- | --- | --- | +| [`allure-assertj`](allure-assertj/README.md) | AssertJ assertions should appear as Allure steps | Requires AspectJ | +| [`allure-hamcrest`](allure-hamcrest/README.md) | Hamcrest assertions should appear as Allure steps | Requires AspectJ | +| [`allure-jupiter-assert`](allure-jupiter-assert/README.md) | JUnit Jupiter assertions should appear as Allure steps | Requires AspectJ | +| [`allure-awaitility`](allure-awaitility/README.md) | Awaitility polling should appear as Allure steps | Register condition listener | +| [`allure-jsonunit`](allure-jsonunit/README.md) | JsonUnit diffs should be attached to Allure | Provides JSON matcher/listener helpers | + +### Core And Support Modules + +| Module | Purpose | +| --- | --- | +| [`allure-bom`](allure-bom/README.md) | Maven/Gradle dependency alignment | +| [`allure-java-commons`](allure-java-commons/README.md) | Runtime API, lifecycle, annotations, aspects, and test-plan filtering | +| [`allure-kotlin-coroutines`](allure-kotlin-coroutines/README.md) | Suspend steps and Allure context propagation for Kotlin coroutines | +| [`allure-kotlin-extensions`](allure-kotlin-extensions/README.md) | Kotlin-first facade for the public Allure runtime API | +| [`allure-model`](allure-model/README.md) | Serializable Allure result model | +| [`allure-descriptions-javadoc`](allure-descriptions-javadoc/README.md) | Annotation processor for JavaDoc-based test descriptions | + +## Common Runtime APIs + +Most adapters depend on `allure-java-commons`, which provides the high-level API: -`RestTemplate` remains supported: -``` -RestTemplate restTemplate = new RestTemplate( - new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()) -); -restTemplate.setInterceptors(Collections.singletonList(new AllureRestTemplate())); -``` +```java +import io.qameta.allure.Allure; -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") +Allure.step("Create order", () -> { + Allure.attachment("request-id", "42"); +}); ``` -## OkHttp +For HTTP clients, browser tools, assertion libraries, and other integrations, add the matching module from the catalog above. Each module README shows the registration snippet for that tool. + +## Resources -Interceptor for OkHttp client, that generates attachment for allure. - -```xml - - io.qameta.allure - allure-okhttp3 - $LATEST_VERSION - -``` - -Usage example: -``` -.addInterceptor(new AllureOkHttp3()) -``` -You can specify custom templates, which should be placed in src/main/resources/tpl folder: -``` -.addInterceptor(new AllureOkHttp3() - .withRequestTemplate("custom-http-request.ftl") - .withResponseTemplate("custom-http-response.ftl")) - -``` - -## gRPC - -Interceptor for gRPC stubs, that generates attachment for allure. - -```xml - - io.qameta.allure - allure-grpc - $LATEST_VERSION - -``` - -Usage example: -``` -.newBlockingStub(channel).withInterceptors(new AllureGrpc()); -``` -You can enable interception of response metadata (disabled by default) -``` -.withInterceptors(new AllureGrpc() - .interceptResponseMetadata(true)) -``` -By default, a step will be marked as failed in case that response contains any statuses except 0(OK). -You can change this behavior, for example, for negative scenarios -``` -.withInterceptors(new AllureGrpc() - .markStepFailedOnNonZeroCode(false)) -``` -You can specify custom templates, which should be placed in src/main/resources/tpl folder: -``` -.withInterceptors(new AllureGrpc() - .setRequestTemplate("custom-http-request.ftl") - .setResponseTemplate("custom-http-response.ftl")) -``` - -## Http client - -Interceptors for Apache HTTP client, that generates attachment for allure. - -```xml - - io.qameta.allure - allure-httpclient - $LATEST_VERSION - -``` - -Usage example: -``` -.addInterceptorFirst(new AllureHttpClientRequest()) -.addInterceptorLast(new AllureHttpClientResponse()); -``` - -## Http client 5 -Interceptors for Apache [httpclient5](https://hc.apache.org/httpcomponents-client-5.2.x/index.html). -Additional info can be found in module `allure-httpclient5` - -```xml - - io.qameta.allure - allure-httpclient5 - $LATEST_VERSION - -``` - -Usage example: -```java -final HttpClientBuilder builder = HttpClientBuilder.create() - .addRequestInterceptorFirst(new AllureHttpClient5Request("your-request-template-attachment.ftl")) - .addResponseInterceptorLast(new AllureHttpClient5Response("your-response-template-attachment.ftl")); -``` - -## JAX-RS Filter - -Filter that can be used with JAX-RS compliant clients such as RESTeasy and Jersey - -```xml - - io.qameta.allure - allure-jax-rs - $LATEST_VERSION - -``` - -Usage example: -``` -.register(AllureJaxRs.class) -``` - -## JsonUnit -JsonPatchMatcher is extension of JsonUnit matcher, that generates pretty html attachment for differences based on [json diff patch](https://github.com/benjamine/jsondiffpatch/blob/master/docs/deltas.md). - -```xml - - io.qameta.allure - allure-jsonunit - $LATEST_VERSION - -``` - -## Awaitility -Extended logging for poling and ignored exceptions for [awaitility](https://github.com/awaitility/awaitility). For -more usage example look into module `allure-awaitility` - -```xml - - io.qameta.allure - allure-awaitility - $LATEST_VERSION - -``` - -Usage example: -``` -Awaitility.setDefaultConditionEvaluationListener(new AllureAwaitilityListener()); -``` - +- [Allure Report documentation](https://allurereport.org/docs/) diff --git a/allure-assertj/README.md b/allure-assertj/README.md new file mode 100644 index 000000000..1c8ca7a1a --- /dev/null +++ b/allure-assertj/README.md @@ -0,0 +1,46 @@ +# allure-assertj + +AssertJ assertion step integration for Allure Java. + +Use this module when you want AssertJ assertion chains to appear as nested steps in Allure Report. + +## Supported Versions + +- Allure Java 3.x requires Java 17 or newer. +- This module targets AssertJ Core. +- The current build validates against AssertJ Core 3.27.7 and AspectJ 1.9.25.1. + +## Installation + +Gradle: + +```kotlin +val aspectjAgent by configurations.creating + +dependencies { + testImplementation(platform("io.qameta.allure:allure-bom:")) + testImplementation("io.qameta.allure:allure-assertj") + testRuntimeOnly("org.aspectj:aspectjrt:") + aspectjAgent("org.aspectj:aspectjweaver:") +} + +tasks.test { + doFirst { + jvmArgs("-javaagent:${aspectjAgent.singleFile}") + } +} +``` + +Maven, with `allure-bom` imported in dependency management, can use the same artifact together with an AspectJ javaagent configured for the test JVM. + +## Setup + +Enable the AspectJ weaver for the test JVM. The module weaves AssertJ calls and reports assertion chains as Allure steps. + +## Report Output + +- AssertJ `Assertions.assertThat(...)` and BDD `then(...)` entry points as assertion steps. +- Assertion chains grouped under the corresponding test or user step. +- Failed assertions update the corresponding Allure step status and status details. + +For large classpaths, consider an AspectJ `META-INF/aop.xml` that limits weaving to Allure, AssertJ, and your test packages. diff --git a/allure-assertj/build.gradle.kts b/allure-assertj/build.gradle.kts index e38cca0ea..6b285a027 100644 --- a/allure-assertj/build.gradle.kts +++ b/allure-assertj/build.gradle.kts @@ -1,23 +1,21 @@ description = "Allure AssertJ Integration" +// Compile the optional type used by the regression fixture without adding it to the test runtime. +val missingDependency by sourceSets.creating + dependencies { api(project(":allure-java-commons")) compileOnly("org.aspectj:aspectjrt") compileOnly("org.assertj:assertj-core") + testImplementation("org.aspectj:aspectjweaver") testImplementation("org.assertj:assertj-core") testImplementation("org.junit.jupiter:junit-jupiter-api") testImplementation("org.slf4j:slf4j-simple") + testCompileOnly(missingDependency.output) 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.assertj" - )) - } + testRuntimeOnly("org.junit.platform:junit-platform-launcher") } tasks.test { 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 33e2a3de6..96128ea92 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 @@ -38,23 +38,16 @@ @Aspect public class AllureAspectJ { - private static InheritableThreadLocal lifecycle = new InheritableThreadLocal() { - @Override - protected AllureLifecycle initialValue() { - return Allure.getLifecycle(); - } - }; - 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*(..))" + + "execution(public static * org.assertj.core.api.Assertions*.assertThat*(..))" + + " || execution(public static * org.assertj.core.api.BDDAssertions*.then*(..))" + + " || execution(public * org.assertj.core.api.*SoftAssertionsProvider+.assertThat*(..))" + + " || execution(public * org.assertj.core.api.*SoftAssertionsProvider+.then*(..))" + ")" ) @@ -67,11 +60,12 @@ public void assertFactoryCall() { @Pointcut( "(" - + "call(public * org.assertj.core.api.AbstractAssert+.*(..))" - + " || call(public * org.assertj.core.api.Assert+.*(..))" - + " || call(public * org.assertj.core.api.Descriptable+.*(..))" + + "execution(public * org.assertj.core.api.AbstractAssert+.*(..))" + + " || execution(public * org.assertj.core.api.Assert+.*(..))" + + " || execution(public * org.assertj.core.api.Descriptable+.*(..))" + ")" + " && target(assertion)" + + " && !execution(* org.assertj.core.api.AssertJProxySetup.*(..))" ) /** @@ -83,16 +77,8 @@ public void assertOperationCall(final AbstractAssert assertion) { //pointcut body, should be empty } - /** - * Handles the user code call callback. - */ - @Pointcut("!within(org.assertj..*) && !within(io.qameta.allure.assertj.AllureAspectJ)") - public void userCodeCall() { - //pointcut body, should be empty - } - @AfterReturning( - pointcut = "assertFactoryCall() && userCodeCall()", + pointcut = "assertFactoryCall()", returning = "result" ) @@ -119,7 +105,7 @@ public void logAssertCreation(final JoinPoint joinPoint, final Object result) { * @return the log assert operation * @throws Throwable if the underlying framework operation fails */ - @Around("assertOperationCall(assertion) && userCodeCall()") + @Around("assertOperationCall(assertion)") public Object logAssertOperation(final ProceedingJoinPoint joinPoint, final AbstractAssert assertion) throws Throwable { @@ -158,27 +144,18 @@ public void softAssertionFailed(final AssertionError error) { getRecorder().softAssertionFailed(error); } - /** - * For tests only. - * - * @param allure allure lifecycle to set. - */ - public static void setLifecycle(final AllureLifecycle allure) { - lifecycle.set(allure); - clearContext(); - } - /** * Returns the lifecycle. * * @return the Allure lifecycle used by this integration */ public static AllureLifecycle getLifecycle() { - return lifecycle.get(); + return Allure.getLifecycle(); } /** - * Handles the clear context callback. + * Drops the calling thread's recorder state. Invoked by {@link AssertJLifecycleListener} at test boundaries so + * chain bookkeeping never accumulates across tests. */ public static void clearContext() { RECORDER.remove(); 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 index c4d30c752..21b850902 100644 --- a/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJChain.java +++ b/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJChain.java @@ -15,6 +15,7 @@ */ package io.qameta.allure.assertj; +import io.qameta.allure.AllureExternalKey; import io.qameta.allure.model.Stage; import io.qameta.allure.model.Status; import io.qameta.allure.model.StatusDetails; @@ -22,7 +23,6 @@ import org.assertj.core.api.AbstractAssert; import java.util.Optional; -import java.util.UUID; /** * Parent Allure step for one AssertJ assertion chain. @@ -69,14 +69,14 @@ final class AssertJChain { private static final String ASSERTJ_STEP_PREFIX = "assert "; - private final String uuid; + private final AllureExternalKey key; private final AbstractAssert assertion; private final StepResult step; AssertJChain(final AbstractAssert assertion, final String subject) { - this.uuid = UUID.randomUUID().toString(); + this.key = AllureExternalKey.random(AllureAspectJ.class); this.assertion = assertion; this.step = new StepResult() .setName(chainName(subject)) @@ -86,8 +86,8 @@ final class AssertJChain { .setStop(System.currentTimeMillis()); } - String getUuid() { - return uuid; + AllureExternalKey getKey() { + return key; } AbstractAssert getAssertion() { 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 index 50bc77531..18794e577 100644 --- a/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJRecorder.java +++ b/allure-assertj/src/main/java/io/qameta/allure/assertj/AssertJRecorder.java @@ -187,8 +187,10 @@ 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()); + lifecycle.getCurrentExecutableKey().ifPresent(parent -> { + lifecycle.startStep(parent, chain.getKey(), chain.getStep()); + lifecycle.stopStep(chain.getKey()); + }); return; } diff --git a/allure-assertj/src/main/java/module-info.java b/allure-assertj/src/main/java/module-info.java new file mode 100644 index 000000000..f0a8ffbe0 --- /dev/null +++ b/allure-assertj/src/main/java/module-info.java @@ -0,0 +1,25 @@ +/* + * 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. + */ +module io.qameta.allure.assertj { + requires transitive io.qameta.allure.commons; + requires org.aspectj.runtime; + requires org.assertj.core; + + exports io.qameta.allure.assertj; + + provides io.qameta.allure.listener.FixtureLifecycleListener with io.qameta.allure.assertj.AssertJLifecycleListener; + provides io.qameta.allure.listener.TestLifecycleListener with io.qameta.allure.assertj.AssertJLifecycleListener; +} diff --git a/allure-assertj/src/missingDependency/java/io/qameta/allure/assertj/fixture/MissingOptionalType.java b/allure-assertj/src/missingDependency/java/io/qameta/allure/assertj/fixture/MissingOptionalType.java new file mode 100644 index 000000000..ddd4209c5 --- /dev/null +++ b/allure-assertj/src/missingDependency/java/io/qameta/allure/assertj/fixture/MissingOptionalType.java @@ -0,0 +1,22 @@ +/* + * 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.fixture; + +public interface MissingOptionalType extends Runnable { + + String getValue(); + +} 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 377e7a6b1..6133fddd7 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,6 +15,7 @@ */ package io.qameta.allure.assertj; +import io.qameta.allure.assertj.fixture.MissingOptionalTypeFixture; import io.qameta.allure.model.Parameter; import io.qameta.allure.model.Status; import io.qameta.allure.model.StatusDetails; @@ -22,6 +23,8 @@ import io.qameta.allure.model.TestResult; import io.qameta.allure.test.AllureFeatures; import io.qameta.allure.test.AllureResults; +import io.qameta.allure.test.IsolatedLifecycle; +import org.aspectj.weaver.Dump; import org.assertj.core.api.AbstractStringAssert; import org.assertj.core.api.InstanceOfAssertFactories; import org.assertj.core.api.SoftAssertions; @@ -35,16 +38,43 @@ import static io.qameta.allure.test.RunUtils.runWithinTestContext; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.tuple; +@IsolatedLifecycle class AllureAspectJTest { + @Test + void shouldNotInspectClassesWithMissingOptionalTypes() { + final ClassLoader classLoader = MissingOptionalTypeFixture.class.getClassLoader(); + assertThatExceptionOfType(ClassNotFoundException.class) + .isThrownBy( + () -> Class.forName( + MissingOptionalTypeFixture.MISSING_TYPE_NAME, + false, + classLoader + ) + ); + + final String dumpBefore = Dump.getLastDumpFileName(); + + final Class fixtureType = MissingOptionalTypeFixture.loadClassWithMissingTypeSignature(); + final String dumpAfter = Dump.getLastDumpFileName(); + + assertThatExceptionOfType(TypeNotPresentException.class) + .isThrownBy(fixtureType::getGenericSuperclass) + .withMessageContaining(MissingOptionalTypeFixture.MISSING_TYPE_NAME); + assertThat(dumpAfter) + .as("last AspectJ dump file") + .isEqualTo(dumpBefore); + } + @AllureFeatures.Steps @Test void shouldCreateSemanticChainForScalarAssert() { final AllureResults results = runWithinTestContext(() -> { assertThat("Data") .hasSize(4); - }, AllureAspectJ::setLifecycle); + }); final TestResult result = assertOnlyOneResult(results); assertThat(result.getSteps()) @@ -67,7 +97,7 @@ void shouldUseAssertDescriptionAsChainName() { assertThat((Object) null) .as("Nullable object") .isNull(); - }, AllureAspectJ::setLifecycle); + }); final TestResult result = assertOnlyOneResult(results); assertThat(result.getSteps()) @@ -87,7 +117,7 @@ void shouldRenderByteArraysWithoutPayload() { 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()) @@ -105,7 +135,7 @@ void shouldRenderCollectionsAsSubjectsAndExpectedValuesAsValues() { final AllureResults results = runWithinTestContext(() -> { assertThat(Arrays.asList("a", "b")) .containsExactly("a", "b"); - }, AllureAspectJ::setLifecycle); + }); final TestResult result = assertOnlyOneResult(results); assertThat(result.getSteps()) @@ -130,7 +160,7 @@ void shouldRenderSmallArraysAsValues() { assertThat(new String[]{"alpha", "bravo"}) .containsExactly("alpha", "bravo"); - }, AllureAspectJ::setLifecycle); + }); final TestResult result = assertOnlyOneResult(results); assertThat(result.getSteps()) @@ -156,7 +186,7 @@ void shouldRenderTuplesAsValues() { tuple("first", Status.PASSED), tuple("second", Status.FAILED) ); - }, AllureAspectJ::setLifecycle); + }); final TestResult result = assertOnlyOneResult(results); assertThat(result.getSteps()) @@ -178,7 +208,7 @@ void shouldRenderNullValuesInContainsExactlyInAnyOrder() { final AllureResults results = runWithinTestContext(() -> { assertThat(Arrays.asList(null, "a", "b")) .containsExactlyInAnyOrder(null, "a", "b"); - }, AllureAspectJ::setLifecycle); + }); final TestResult result = assertOnlyOneResult(results); assertThat(result.getSteps()) @@ -203,7 +233,7 @@ void shouldRenderNullValuesAfterExtractingAndKeepLambdaVarargs() { assertThat(model) .extracting(TestResult::getDescription, TestResult::getDescriptionHtml) .containsExactly(null, null); - }, AllureAspectJ::setLifecycle); + }); final TestResult result = assertOnlyOneResult(results); assertThat(result.getSteps()) @@ -231,7 +261,7 @@ void shouldRenderFieldOrPropertyValueAssertions() { final AllureResults results = runWithinTestContext(() -> { assertThat(details) .hasFieldOrPropertyWithValue("message", "Make the test failed"); - }, AllureAspectJ::setLifecycle); + }); final TestResult result = assertOnlyOneResult(results); assertThat(result.getSteps()) @@ -255,7 +285,7 @@ void shouldTruncateLongStepNamesAndAddOnlyTruncatedValuesAsParameters() { final AllureResults results = runWithinTestContext(() -> { assertThat(value) .isEqualTo(value); - }, AllureAspectJ::setLifecycle); + }); final TestResult result = assertOnlyOneResult(results); assertThat(result.getSteps()) @@ -294,7 +324,7 @@ void shouldCreateSeparateChainsForMultipleAssertThatCalls() { assertThat(Arrays.asList("a", "b")) .hasSize(2) .contains("a"); - }, AllureAspectJ::setLifecycle); + }); final TestResult result = assertOnlyOneResult(results); assertThat(result.getSteps()) @@ -328,7 +358,7 @@ void shouldAttachOperationsToStoredAssertionInstances() { a.isEqualTo("alpha"); b.isEqualTo("bravo"); - }, AllureAspectJ::setLifecycle); + }); final TestResult result = assertOnlyOneResult(results); assertThat(result.getSteps()) @@ -361,7 +391,7 @@ void shouldAvoidVerboseModelToStringPayloads() { assertThat(Collections.singletonList(model)) .hasSize(1) .containsExactly(model); - }, AllureAspectJ::setLifecycle); + }); final TestResult result = assertOnlyOneResult(results); assertThat(result.getSteps()) @@ -408,7 +438,7 @@ void shouldKeepNavigationInsideTheSameChain() { assertThat(Collections.singletonList(Collections.singletonList("delta"))) .flatExtracting(value -> value) .containsExactly("delta"); - }, AllureAspectJ::setLifecycle); + }); final TestResult result = assertOnlyOneResult(results); assertThat(result.getSteps()) @@ -440,7 +470,7 @@ void shouldRenderSerializedLambdaMethodReferences() { assertThat(Collections.singletonList(model)) .extracting((Function & Serializable) TestResult::getFullName) .containsExactly("my.company.Test.testOne"); - }, AllureAspectJ::setLifecycle); + }); final TestResult result = assertOnlyOneResult(results); assertThat(result.getSteps()) @@ -458,7 +488,7 @@ void shouldMarkTheFailedHardAssertionOperation() { final AllureResults results = runWithinTestContext(() -> { assertThat("Data") .hasSize(5); - }, AllureAspectJ::setLifecycle); + }); final TestResult result = assertOnlyOneResult(results); assertThat(result.getSteps()) @@ -486,7 +516,7 @@ void shouldMarkTheFailedSoftAssertionOperationBeforeAssertAll() { .as("Age") .isEqualTo(26); soft.assertAll(); - }, AllureAspectJ::setLifecycle); + }); final TestResult result = assertOnlyOneResult(results); assertThat(result.getSteps()) @@ -518,7 +548,7 @@ void shouldAttachNestedAssertionsUnderCallbackOperations() { .startsWith("al") .endsWith("ha") ); - }, AllureAspectJ::setLifecycle); + }); final TestResult result = assertOnlyOneResult(results); assertThat(result.getSteps()) diff --git a/allure-assertj/src/test/java/io/qameta/allure/assertj/fixture/MissingOptionalTypeFixture.java b/allure-assertj/src/test/java/io/qameta/allure/assertj/fixture/MissingOptionalTypeFixture.java new file mode 100644 index 000000000..a39a9c164 --- /dev/null +++ b/allure-assertj/src/test/java/io/qameta/allure/assertj/fixture/MissingOptionalTypeFixture.java @@ -0,0 +1,73 @@ +/* + * 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.fixture; + +import java.util.function.Function; + +/** + * Loads a bounded generic subclass whose type argument is deliberately absent at runtime. + */ +public final class MissingOptionalTypeFixture { + + public static final String MISSING_TYPE_NAME = "io.qameta.allure.assertj.fixture.MissingOptionalType"; + + private MissingOptionalTypeFixture() { + } + + public static Class loadClassWithMissingTypeSignature() { + return MissingTypeProperties.class; + } + + private interface TypeProperties { + + Class getType(); + + String get(T instance); + + } + + private static class MappedProperties implements TypeProperties { + + @Override + public Class getType() { + return null; + } + + protected void add(final Function getter) { + //fixture method, should be empty + } + + @Override + public String get(final T instance) { + return null; + } + + } + + private static final class MissingTypeProperties extends MappedProperties { + + @Override + public Class getType() { + return null; + } + + private MissingTypeProperties() { + add(MissingOptionalType::getValue); + } + + } + +} diff --git a/allure-attachments/src/main/java/io/qameta/allure/attachment/AttachmentContent.java b/allure-attachments/src/main/java/io/qameta/allure/attachment/AttachmentContent.java deleted file mode 100644 index f3c57536b..000000000 --- a/allure-attachments/src/main/java/io/qameta/allure/attachment/AttachmentContent.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.attachment; - -/** - * Defines the attachment content contract used by Allure attachment support. - * - *

Implement this interface when custom code needs to participate in the same integration flow as the built-in Allure adapter components.

- */ -public interface AttachmentContent { - - /** - * Returns the content. - * - * @return the content - */ - String getContent(); - - /** - * Returns the content type. - * - * @return the content type - */ - String getContentType(); - - /** - * Returns the file extension. - * - * @return the file extension - */ - String getFileExtension(); - -} diff --git a/allure-attachments/src/main/java/io/qameta/allure/attachment/AttachmentRenderException.java b/allure-attachments/src/main/java/io/qameta/allure/attachment/AttachmentRenderException.java deleted file mode 100644 index b097a0e16..000000000 --- a/allure-attachments/src/main/java/io/qameta/allure/attachment/AttachmentRenderException.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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.attachment; - -/** - * Supports Allure attachment integration with Allure reporting. - * - *

Use this type through the module that owns it when translating framework execution, result metadata, or attachments into Allure report data.

- */ -public class AttachmentRenderException extends RuntimeException { - - /** - * Creates an attachment render exception with the supplied values. - * - * @param message the message - * @param cause the failure cause reported by the framework - */ - public AttachmentRenderException(final String message, final Throwable cause) { - super(message, cause); - } -} diff --git a/allure-attachments/src/main/java/io/qameta/allure/attachment/DefaultAttachmentContent.java b/allure-attachments/src/main/java/io/qameta/allure/attachment/DefaultAttachmentContent.java deleted file mode 100644 index fb70c14f3..000000000 --- a/allure-attachments/src/main/java/io/qameta/allure/attachment/DefaultAttachmentContent.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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.attachment; - -/** - * Supports Allure attachment integration with Allure reporting. - * - *

Use this type through the module that owns it when translating framework execution, result metadata, or attachments into Allure report data.

- */ -public class DefaultAttachmentContent implements AttachmentContent { - - private final String content; - - private final String contentType; - - private final String fileExtension; - - /** - * Creates a default attachment content with the supplied values. - * - * @param content the attachment content - * @param contentType the attachment content type - * @param fileExtension the attachment file extension - */ - public DefaultAttachmentContent(final String content, - final String contentType, - final String fileExtension) { - this.content = content; - this.contentType = contentType; - this.fileExtension = fileExtension; - } - - /** - * {@inheritDoc} - */ - @Override - public String getContent() { - return content; - } - - /** - * {@inheritDoc} - */ - @Override - public String getContentType() { - return contentType; - } - - /** - * {@inheritDoc} - */ - @Override - public String getFileExtension() { - return fileExtension; - } -} diff --git a/allure-attachments/src/main/java/io/qameta/allure/attachment/DefaultAttachmentProcessor.java b/allure-attachments/src/main/java/io/qameta/allure/attachment/DefaultAttachmentProcessor.java deleted file mode 100644 index 3bfc71a43..000000000 --- a/allure-attachments/src/main/java/io/qameta/allure/attachment/DefaultAttachmentProcessor.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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.attachment; - -import io.qameta.allure.Allure; -import io.qameta.allure.AllureLifecycle; - -import java.nio.charset.StandardCharsets; - -/** - * Supports Allure attachment integration with Allure reporting. - * - *

Use this type through the module that owns it when translating framework execution, result metadata, or attachments into Allure report data.

- */ -public class DefaultAttachmentProcessor implements AttachmentProcessor { - - private final AllureLifecycle lifecycle; - - /** - * Creates a default attachment processor with default configuration. - */ - public DefaultAttachmentProcessor() { - this(Allure.getLifecycle()); - } - - /** - * Creates a default attachment processor with the supplied values. - * - * @param lifecycle the Allure lifecycle to use - */ - public DefaultAttachmentProcessor(final AllureLifecycle lifecycle) { - this.lifecycle = lifecycle; - } - - /** - * {@inheritDoc} - */ - @Override - public void addAttachment(final AttachmentData attachmentData, - final AttachmentRenderer renderer) { - final AttachmentContent content = renderer.render(attachmentData); - lifecycle.addAttachment( - attachmentData.getName(), - content.getContentType(), - content.getFileExtension(), - content.getContent().getBytes(StandardCharsets.UTF_8) - ); - } -} diff --git a/allure-attachments/src/main/java/io/qameta/allure/attachment/FreemarkerAttachmentRenderer.java b/allure-attachments/src/main/java/io/qameta/allure/attachment/FreemarkerAttachmentRenderer.java deleted file mode 100644 index 6442cb62f..000000000 --- a/allure-attachments/src/main/java/io/qameta/allure/attachment/FreemarkerAttachmentRenderer.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.attachment; - -import freemarker.template.Configuration; -import freemarker.template.Template; -import freemarker.template.TemplateExceptionHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.StringWriter; -import java.io.Writer; -import java.util.Collections; - -/** - * Supports Allure attachment integration with Allure reporting. - * - *

Use this type through the module that owns it when translating framework execution, result metadata, or attachments into Allure report data.

- */ -public class FreemarkerAttachmentRenderer implements AttachmentRenderer { - - private static final Logger LOGGER = LoggerFactory.getLogger(FreemarkerAttachmentRenderer.class); - - private final Configuration configuration; - - private final String templateName; - - /** - * Creates a freemarker attachment renderer with the supplied values. - * - * @param templateName the template name - */ - public FreemarkerAttachmentRenderer(final String templateName) { - this.templateName = templateName; - this.configuration = new Configuration(Configuration.VERSION_2_3_23); - this.configuration.setLocalizedLookup(false); - this.configuration.setTemplateUpdateDelayMilliseconds(0); - this.configuration.setTemplateExceptionHandler(TemplateExceptionHandler.HTML_DEBUG_HANDLER); - this.configuration.setClassLoaderForTemplateLoading(getClass().getClassLoader(), "tpl"); - } - - /** - * {@inheritDoc} - */ - @Override - public DefaultAttachmentContent render(final AttachmentData data) { - try (Writer writer = new StringWriter()) { - final Template template = configuration.getTemplate(templateName); - template.process(Collections.singletonMap("data", data), writer); - return new DefaultAttachmentContent(writer.toString(), "text/html", ".html"); - } catch (Exception e) { - LOGGER.debug(data.toString()); - throw new AttachmentRenderException("Could't render http attachment file", e); - } - } - -} diff --git a/allure-attachments/src/main/java/io/qameta/allure/attachment/http/HttpRequestAttachment.java b/allure-attachments/src/main/java/io/qameta/allure/attachment/http/HttpRequestAttachment.java deleted file mode 100644 index b58f9c18b..000000000 --- a/allure-attachments/src/main/java/io/qameta/allure/attachment/http/HttpRequestAttachment.java +++ /dev/null @@ -1,406 +0,0 @@ -/* - * 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.attachment.http; - -import io.qameta.allure.attachment.AttachmentData; -import io.qameta.allure.util.ObjectUtils; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - -/** - * Describes an HTTP or RPC request attachment rendered in an Allure report. - * - *

Use this model to carry request metadata and body content from client interceptors to attachment renderers and processors.

- */ -public class HttpRequestAttachment implements AttachmentData { - - private final String name; - - private final String url; - - private final String method; - - private final String body; - - private final String curl; - - private final Map headers; - - private final Map cookies; - - private final Map formParams; - - /** - * Creates an HTTP request attachment with the supplied values. - * - * @param name the display name or logical name to use - * @param url the request URL or service method name - * @param method the framework or Java method to inspect - * @param body the attachment body - * @param curl the curl - * @param headers the headers - * @param cookies the cookies - */ - public HttpRequestAttachment(final String name, final String url, final String method, - final String body, final String curl, final Map headers, - final Map cookies) { - this(name, url, method, body, curl, headers, cookies, Collections.emptyMap()); - } - - /** - * Creates an HTTP request attachment with the supplied values. - * - * @param name the display name or logical name to use - * @param url the request URL or service method name - * @param method the framework or Java method to inspect - * @param body the attachment body - * @param curl the curl - * @param headers the headers - * @param cookies the cookies - * @param formParams the form params - */ - @SuppressWarnings("checkstyle:parameternumber") - public HttpRequestAttachment(final String name, final String url, final String method, - final String body, final String curl, final Map headers, - final Map cookies, final Map formParams) { - this.name = name; - this.url = url; - this.method = method; - this.body = body; - this.curl = curl; - this.headers = headers; - this.cookies = cookies; - this.formParams = formParams; - } - - /** - * Returns the url. - * - * @return the url - */ - public String getUrl() { - return url; - } - - /** - * Returns the method. - * - * @return the method - */ - public String getMethod() { - return method; - } - - /** - * Returns the body. - * - * @return the body - */ - public String getBody() { - return body; - } - - /** - * Returns the headers. - * - * @return the headers - */ - public Map getHeaders() { - return headers; - } - - /** - * Returns the cookies. - * - * @return the cookies - */ - public Map getCookies() { - return cookies; - } - - /** - * Returns the form params. - * - * @return the form params - */ - public Map getFormParams() { - return formParams; - } - - /** - * Returns the curl. - * - * @return the curl - */ - public String getCurl() { - return curl; - } - - /** - * {@inheritDoc} - */ - @Override - public String getName() { - return name; - } - - /** - * {@inheritDoc} - */ - @Override - public String toString() { - return "HttpRequestAttachment(" - + "\n\tname=" + this.name - + ",\n\turl=" + this.url - + ",\n\tbody=" + this.body - + ",\n\theaders=" + ObjectUtils.mapToString(this.headers) - + ",\n\tcookies=" + ObjectUtils.mapToString(this.cookies) - + ",\n\tformParams=" + ObjectUtils.mapToString(this.formParams) - + "\n)"; - } - - /** - * Builder for HttpRequestAttachment. - */ - public static final class Builder { - - private final String name; - - private final String url; - - private String method; - - private String body; - - private final Map headers = new HashMap<>(); - - private final Map cookies = new HashMap<>(); - - private final Map formParams = new HashMap<>(); - - private Builder(final String name, final String url) { - Objects.requireNonNull(name, "Name must not be null value"); - Objects.requireNonNull(url, "Url must not be null value"); - this.name = name; - this.url = url; - } - - /** - * Creates a builder for a builder. - * - * @param attachmentName the attachment display name - * @param url the request URL or service method name - * @return a new builder instance - */ - public static Builder create(final String attachmentName, final String url) { - return new Builder(attachmentName, url); - } - - /** - * Sets the method. - * - * @param method the framework or Java method to inspect - * @return this instance for method chaining - */ - public Builder setMethod(final String method) { - Objects.requireNonNull(method, "Method must not be null value"); - this.method = method; - return this; - } - - /** - * Sets the header. - * - * @param name the display name or logical name to use - * @param value the value to set - * @return this instance for method chaining - */ - public Builder setHeader(final String name, final String value) { - Objects.requireNonNull(name, "Header name must not be null value"); - Objects.requireNonNull(value, "Header value must not be null value"); - this.headers.put(name, value); - return this; - } - - /** - * Sets the headers. - * - * @param headers the headers - * @return this instance for method chaining - */ - public Builder setHeaders(final Map headers) { - Objects.requireNonNull(headers, "Headers must not be null value"); - this.headers.putAll(headers); - return this; - } - - /** - * Sets the cookie. - * - * @param name the display name or logical name to use - * @param value the value to set - * @return this instance for method chaining - */ - public Builder setCookie(final String name, final String value) { - Objects.requireNonNull(name, "Cookie name must not be null value"); - Objects.requireNonNull(value, "Cookie value must not be null value"); - this.cookies.put(name, value); - return this; - } - - /** - * Sets the cookies. - * - * @param cookies the cookies - * @return this instance for method chaining - */ - public Builder setCookies(final Map cookies) { - Objects.requireNonNull(cookies, "Cookies must not be null value"); - this.cookies.putAll(cookies); - return this; - } - - /** - * Sets the body. - * - * @param body the attachment body - * @return this instance for method chaining - */ - public Builder setBody(final String body) { - Objects.requireNonNull(body, "Body should not be null value"); - this.body = body; - return this; - } - - /** - * Sets the form params. - * - * @param formParams the form params - * @return this instance for method chaining - */ - public Builder setFormParams(final Map formParams) { - Objects.requireNonNull(formParams, "Form params must not be null value"); - this.formParams.putAll(formParams); - return this; - } - - /** - * Use setter method instead. - * @deprecated scheduled for removal in 3.0 release - */ - @Deprecated - public Builder withMethod(final String method) { - return setMethod(method); - } - - /** - * Use setter method instead. - * @deprecated scheduled for removal in 3.0 release - */ - @Deprecated - public Builder withHeader(final String name, final String value) { - return setHeader(name, value); - } - - /** - * Use setter method instead. - * @deprecated scheduled for removal in 3.0 release - */ - @Deprecated - public Builder withHeaders(final Map headers) { - return setHeaders(headers); - } - - /** - * Use setter method instead. - * @deprecated scheduled for removal in 3.0 release - */ - @Deprecated - public Builder withCookie(final String name, final String value) { - return setCookie(name, value); - } - - /** - * Use setter method instead. - * @deprecated scheduled for removal in 3.0 release - */ - @Deprecated - public Builder withCookies(final Map cookies) { - return setCookies(cookies); - } - - /** - * Use setter method instead. - * @deprecated scheduled for removal in 3.0 release - */ - @Deprecated - public Builder withBody(final String body) { - return setBody(body); - } - - /** - * Builds a builder from the configured values. - * - * @return the built attachment model - */ - public HttpRequestAttachment build() { - return new HttpRequestAttachment(name, url, method, body, getCurl(), headers, cookies, formParams); - } - - private String getCurl() { - final StringBuilder builder = new StringBuilder("curl -v"); - if (Objects.nonNull(method)) { - builder.append(" -X ").append(method); - } - builder.append(" '").append(url).append('\''); - headers.forEach((key, value) -> appendHeader(builder, key, value)); - cookies.forEach((key, value) -> appendCookie(builder, key, value)); - formParams.forEach((key, value) -> appendFormParams(builder, key, value)); - - if (Objects.nonNull(body)) { - builder.append(" -d '").append(body).append('\''); - } - return builder.toString(); - } - - private static void appendHeader(final StringBuilder builder, final String key, final String value) { - builder.append(" -H '") - .append(key) - .append(": ") - .append(value) - .append('\''); - } - - private static void appendCookie(final StringBuilder builder, final String key, final String value) { - builder.append(" -b '") - .append(key) - .append('=') - .append(value) - .append('\''); - } - - private static void appendFormParams(final StringBuilder builder, final String key, final String value) { - builder.append(" --form '") - .append(key) - .append('=') - .append(value) - .append('\''); - } - } -} diff --git a/allure-attachments/src/main/java/io/qameta/allure/attachment/http/HttpResponseAttachment.java b/allure-attachments/src/main/java/io/qameta/allure/attachment/http/HttpResponseAttachment.java deleted file mode 100644 index 4a956dba9..000000000 --- a/allure-attachments/src/main/java/io/qameta/allure/attachment/http/HttpResponseAttachment.java +++ /dev/null @@ -1,331 +0,0 @@ -/* - * 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.attachment.http; - -import io.qameta.allure.attachment.AttachmentData; -import io.qameta.allure.util.ObjectUtils; - -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - -/** - * Describes an HTTP or RPC response attachment rendered in an Allure report. - * - *

Use this model to carry response metadata and body content from client interceptors to attachment renderers and processors.

- */ -public class HttpResponseAttachment implements AttachmentData { - - private final String name; - - private final String url; - - private final String body; - - private final int responseCode; - - private final Map headers; - - private final Map cookies; - - /** - * Creates an HTTP response attachment with the supplied values. - * - * @param name the display name or logical name to use - * @param url the request URL or service method name - * @param body the attachment body - * @param responseCode the response code - * @param headers the headers - * @param cookies the cookies - */ - public HttpResponseAttachment(final String name, final String url, - final String body, final int responseCode, - final Map headers, final Map cookies) { - this.name = name; - this.url = url; - this.body = body; - this.responseCode = responseCode; - this.headers = headers; - this.cookies = cookies; - } - - /** - * {@inheritDoc} - */ - @Override - public String getName() { - return name; - } - - /** - * Returns the url. - * - * @return the url - */ - public String getUrl() { - return url; - } - - /** - * Returns the body. - * - * @return the body - */ - public String getBody() { - return body; - } - - /** - * Returns the response code. - * - * @return the response code - */ - public int getResponseCode() { - return responseCode; - } - - /** - * Returns the headers. - * - * @return the headers - */ - public Map getHeaders() { - return headers; - } - - /** - * Returns the cookies. - * - * @return the cookies - */ - public Map getCookies() { - return cookies; - } - - /** - * {@inheritDoc} - */ - @Override - public String toString() { - return "HttpResponseAttachment(" - + "\n\tname=" + this.name - + ",\n\turl=" + this.url - + ",\n\tbody=" + this.body - + ",\n\tresponseCode=" + this.responseCode - + ",\n\theaders=" + ObjectUtils.mapToString(this.headers) - + ",\n\tcookies=" + ObjectUtils.mapToString(this.cookies) - + "\n)"; - } - - /** - * Builder for HttpRequestAttachment. - */ - public static final class Builder { - - private final String name; - - private String url; - - private int responseCode; - - private String body; - - private final Map headers = new HashMap<>(); - - private final Map cookies = new HashMap<>(); - - private Builder(final String name) { - Objects.requireNonNull(name, "Name must not be null value"); - this.name = name; - } - - /** - * Creates a builder for a builder. - * - * @param attachmentName the attachment display name - * @return a new builder instance - */ - public static Builder create(final String attachmentName) { - return new Builder(attachmentName); - } - - /** - * Sets the url. - * - * @param url the request URL or service method name - * @return this instance for method chaining - */ - public Builder setUrl(final String url) { - Objects.requireNonNull(url, "Url must not be null value"); - this.url = url; - return this; - } - - /** - * Sets the response code. - * - * @param responseCode the response code - * @return this instance for method chaining - */ - public Builder setResponseCode(final int responseCode) { - this.responseCode = responseCode; - return this; - } - - /** - * Sets the header. - * - * @param name the display name or logical name to use - * @param value the value to set - * @return this instance for method chaining - */ - public Builder setHeader(final String name, final String value) { - Objects.requireNonNull(name, "Header name must not be null value"); - Objects.requireNonNull(value, "Header value must not be null value"); - this.headers.put(name, value); - return this; - } - - /** - * Sets the headers. - * - * @param headers the headers - * @return this instance for method chaining - */ - public Builder setHeaders(final Map headers) { - Objects.requireNonNull(headers, "Headers must not be null value"); - this.headers.putAll(headers); - return this; - } - - /** - * Sets the cookie. - * - * @param name the display name or logical name to use - * @param value the value to set - * @return this instance for method chaining - */ - public Builder setCookie(final String name, final String value) { - Objects.requireNonNull(name, "Cookie name must not be null value"); - Objects.requireNonNull(value, "Cookie value must not be null value"); - this.cookies.put(name, value); - return this; - } - - /** - * Sets the cookies. - * - * @param cookies the cookies - * @return this instance for method chaining - */ - public Builder setCookies(final Map cookies) { - Objects.requireNonNull(cookies, "Cookies must not be null value"); - this.cookies.putAll(cookies); - return this; - } - - /** - * Sets the body. - * - * @param body the attachment body - * @return this instance for method chaining - */ - public Builder setBody(final String body) { - Objects.requireNonNull(body, "Body should not be null value"); - this.body = body; - return this; - } - - /** - * Use setter method instead. - * - * @deprecated scheduled for removal in 3.0 release - */ - @Deprecated - public Builder withUrl(final String url) { - return setUrl(url); - } - - /** - * Use setter method instead. - * - * @deprecated scheduled for removal in 3.0 release - */ - @Deprecated - public Builder withResponseCode(final int responseCode) { - return setResponseCode(responseCode); - } - - /** - * Use setter method instead. - * - * @deprecated scheduled for removal in 3.0 release - */ - @Deprecated - public Builder withHeader(final String name, final String value) { - return setHeader(name, value); - } - - /** - * Use setter method instead. - * - * @deprecated scheduled for removal in 3.0 release - */ - @Deprecated - public Builder withHeaders(final Map headers) { - return setHeaders(headers); - } - - /** - * Use setter method instead. - * - * @deprecated scheduled for removal in 3.0 release - */ - @Deprecated - public Builder withCookie(final String name, final String value) { - return setCookie(name, value); - } - - /** - * Use setter method instead. - * - * @deprecated scheduled for removal in 3.0 release - */ - @Deprecated - public Builder withCookies(final Map cookies) { - return setCookies(cookies); - } - - /** - * Use setter method instead. - * - * @deprecated scheduled for removal in 3.0 release - */ - @Deprecated - public Builder withBody(final String body) { - return setBody(body); - } - - /** - * Builds a builder from the configured values. - * - * @return the built attachment model - */ - public HttpResponseAttachment build() { - return new HttpResponseAttachment(name, url, body, responseCode, headers, cookies); - } - } -} diff --git a/allure-attachments/src/main/resources/tpl/http-request.ftl b/allure-attachments/src/main/resources/tpl/http-request.ftl deleted file mode 100644 index 935c1c91a..000000000 --- a/allure-attachments/src/main/resources/tpl/http-request.ftl +++ /dev/null @@ -1,47 +0,0 @@ -<#ftl output_format="HTML"> -<#-- @ftlvariable name="data" type="io.qameta.allure.attachment.http.HttpRequestAttachment" --> -
<#if data.method??>${data.method}<#else>GET to <#if data.url??>${data.url}<#else>Unknown
- -<#if data.body??> -

Body

-
-
-    <#t>${data.body}
-    
-
- - -<#if (data.headers)?has_content> -

Headers

-
- <#list data.headers as name, value> -
${name}: ${value!"null"}
- -
- - - -<#if (data.cookies)?has_content> -

Cookies

-
- <#list data.cookies as name, value> -
${name}: ${value!"null"}
- -
- - -<#if data.curl??> -

Curl

-
- ${data.curl} -
- - -<#if (data.formParams)?has_content> -

FormParams

-
- <#list data.formParams as name, value> -
${name}: ${value!"null"}
- -
- diff --git a/allure-attachments/src/main/resources/tpl/http-response.ftl b/allure-attachments/src/main/resources/tpl/http-response.ftl deleted file mode 100644 index 91fed7287..000000000 --- a/allure-attachments/src/main/resources/tpl/http-response.ftl +++ /dev/null @@ -1,32 +0,0 @@ -<#ftl output_format="HTML"> -<#-- @ftlvariable name="data" type="io.qameta.allure.attachment.http.HttpResponseAttachment" --> -
Status code <#if data.responseCode??>${data.responseCode} <#else>Unknown
-<#if data.url??>
${data.url}
- -<#if data.body??> -

Body

-
-
-    <#t>${data.body}
-    
-
- - -<#if (data.headers)?has_content> -

Headers

-
- <#list data.headers as name, value> -
${name}: ${value!"null"}
- -
- - - -<#if (data.cookies)?has_content> -

Cookies

-
- <#list data.cookies as name, value> -
${name}: ${value!"null"}
- -
- diff --git a/allure-attachments/src/test/java/io/qameta/allure/attachment/DefaultAttachmentProcessorTest.java b/allure-attachments/src/test/java/io/qameta/allure/attachment/DefaultAttachmentProcessorTest.java deleted file mode 100644 index f1defa786..000000000 --- a/allure-attachments/src/test/java/io/qameta/allure/attachment/DefaultAttachmentProcessorTest.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.attachment; - -import io.qameta.allure.AllureLifecycle; -import io.qameta.allure.attachment.http.HttpRequestAttachment; -import io.qameta.allure.test.AllureFeatures; -import org.junit.jupiter.api.Test; - -import java.nio.charset.StandardCharsets; - -import static io.qameta.allure.attachment.testdata.TestData.randomAttachmentContent; -import static io.qameta.allure.attachment.testdata.TestData.randomHttpRequestAttachment; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -class DefaultAttachmentProcessorTest { - - @SuppressWarnings("unchecked") - @AllureFeatures.Attachments - @Test - void shouldProcessAttachments() { - final HttpRequestAttachment attachment = randomHttpRequestAttachment(); - final AllureLifecycle lifecycle = mock(AllureLifecycle.class); - final AttachmentRenderer renderer = mock(AttachmentRenderer.class); - final AttachmentContent content = randomAttachmentContent(); - doReturn(content) - .when(renderer) - .render(attachment); - - new DefaultAttachmentProcessor(lifecycle) - .addAttachment(attachment, renderer); - - verify(renderer, times(1)).render(attachment); - verify(lifecycle, times(1)) - .addAttachment( - eq(attachment.getName()), - eq(content.getContentType()), - eq(content.getFileExtension()), - eq(content.getContent().getBytes(StandardCharsets.UTF_8)) - ); - } -} diff --git a/allure-attachments/src/test/java/io/qameta/allure/attachment/FreemarkerAttachmentRendererTest.java b/allure-attachments/src/test/java/io/qameta/allure/attachment/FreemarkerAttachmentRendererTest.java deleted file mode 100644 index 956080c3b..000000000 --- a/allure-attachments/src/test/java/io/qameta/allure/attachment/FreemarkerAttachmentRendererTest.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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.attachment; - -import io.qameta.allure.attachment.http.HttpRequestAttachment; -import io.qameta.allure.attachment.http.HttpResponseAttachment; -import io.qameta.allure.test.AllureFeatures; -import org.junit.jupiter.api.Test; - -import static io.qameta.allure.attachment.testdata.TestData.randomHttpRequestAttachment; -import static io.qameta.allure.attachment.testdata.TestData.randomHttpResponseAttachment; -import static org.assertj.core.api.Assertions.assertThat; -class FreemarkerAttachmentRendererTest { - - private static final String CONTENT = "content"; - private static final String CONTENT_TYPE = "contentType"; - private static final String TEXT_HTML = "text/html"; - private static final String FILE_EXTENSION = "fileExtension"; - private static final String HTML = ".html"; - - @AllureFeatures.Attachments - @Test - void shouldRenderRequestAttachment() { - final HttpRequestAttachment data = randomHttpRequestAttachment(); - final DefaultAttachmentContent content = new FreemarkerAttachmentRenderer("http-request.ftl") - .render(data); - - assertThat(content) - .hasFieldOrPropertyWithValue(CONTENT_TYPE, TEXT_HTML) - .hasFieldOrPropertyWithValue(FILE_EXTENSION, HTML) - .hasFieldOrProperty(CONTENT); - } - - @AllureFeatures.Attachments - @Test - void shouldRenderResponseAttachment() { - final HttpResponseAttachment data = randomHttpResponseAttachment(); - final DefaultAttachmentContent content = new FreemarkerAttachmentRenderer("http-response.ftl") - .render(data); - - assertThat(content) - .hasFieldOrPropertyWithValue(CONTENT_TYPE, TEXT_HTML) - .hasFieldOrPropertyWithValue(FILE_EXTENSION, HTML) - .hasFieldOrProperty(CONTENT); - } -} diff --git a/allure-attachments/src/test/java/io/qameta/allure/attachment/NegativeFreemarkerAttachmentRendererTest.java b/allure-attachments/src/test/java/io/qameta/allure/attachment/NegativeFreemarkerAttachmentRendererTest.java deleted file mode 100644 index 5f5ba6d4f..000000000 --- a/allure-attachments/src/test/java/io/qameta/allure/attachment/NegativeFreemarkerAttachmentRendererTest.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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.attachment; - -import io.qameta.allure.attachment.http.HttpRequestAttachment; -import io.qameta.allure.test.AllureFeatures; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.io.ByteArrayOutputStream; -import java.io.PrintStream; -import java.io.UnsupportedEncodingException; -import java.nio.charset.StandardCharsets; - -import static io.qameta.allure.attachment.testdata.TestData.negativeHttpRequestAttachment; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -class NegativeFreemarkerAttachmentRendererTest { - - private static final String TEMPLATE_FOR_EXCEPTION = "body-npe-non-safe-attachment.ftl"; - - private PrintStream realSysOut; - private ByteArrayOutputStream sysOutBuffer; - - @BeforeEach - void setUpSysOut() throws UnsupportedEncodingException { - realSysOut = System.err; - sysOutBuffer = new ByteArrayOutputStream(); - System.setErr(new PrintStream(sysOutBuffer, false, StandardCharsets.UTF_8.toString())); - } - - @AfterEach - void rollBackSysOut() { - System.setErr(realSysOut); - } - - @AllureFeatures.Attachments - @Test - void shouldThrowExceptionalSituationsForFreeMarketRendererWithIncorrectAttachmentData() { - assertThrows(AttachmentRenderException.class, () -> { - final HttpRequestAttachment data = negativeHttpRequestAttachment(); - new FreemarkerAttachmentRenderer(TEMPLATE_FOR_EXCEPTION).render(data); - }); - } - - @AllureFeatures.Attachments - @Test - void shouldExplainExceptionalSituationsForFreeMarketRenderer() throws UnsupportedEncodingException { - try { - final HttpRequestAttachment data = negativeHttpRequestAttachment(); - new FreemarkerAttachmentRenderer(TEMPLATE_FOR_EXCEPTION).render(data); - } catch (Exception ignored) { - // for test purposes - } - assertThat(sysOutBuffer.toString(StandardCharsets.UTF_8.toString())) - .contains("SEVERE: Error executing FreeMarker template") - .contains("FreeMarker template error:") - .contains("The following has evaluated to null or missing:") - .contains("==> data.body") - .contains("[in template \"body-npe-non-safe-attachment.ftl\" at line 8, column 11]") - .contains("\t- Failed at: ${data.body.size}") - .contains("io.qameta.allure.attachment.FreemarkerAttachmentRenderer - HttpRequestAttachment") - .contains("\tname=null,") - .contains("\turl=null,") - .contains("\tbody=null,") - .contains("\theaders={},") - .contains("\tcookies={}"); - } -} diff --git a/allure-attachments/src/test/java/io/qameta/allure/attachment/testdata/TestData.java b/allure-attachments/src/test/java/io/qameta/allure/attachment/testdata/TestData.java deleted file mode 100644 index 3ef9c4c63..000000000 --- a/allure-attachments/src/test/java/io/qameta/allure/attachment/testdata/TestData.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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.attachment.testdata; - -import io.qameta.allure.attachment.AttachmentContent; -import io.qameta.allure.attachment.DefaultAttachmentContent; -import io.qameta.allure.attachment.http.HttpRequestAttachment; -import io.qameta.allure.attachment.http.HttpResponseAttachment; -import org.apache.commons.lang3.RandomStringUtils; - -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ThreadLocalRandom; -public final class TestData { - - private TestData() { - throw new IllegalStateException("Do not instance"); - } - - public static String randomString() { - return RandomStringUtils.randomAlphabetic(10); - } - - public static HttpRequestAttachment randomHttpRequestAttachment() { - return new HttpRequestAttachment( - randomString(), - randomString(), - randomString(), - randomString(), - randomString(), - randomMap(), - randomMap() - ); - } - - public static HttpResponseAttachment randomHttpResponseAttachment() { - return new HttpResponseAttachment( - randomString(), - randomString(), - randomString(), - ThreadLocalRandom.current().nextInt(), - randomMap(), - randomMap() - ); - } - - public static AttachmentContent randomAttachmentContent() { - return new DefaultAttachmentContent(randomString(), randomString(), randomString()); - } - - public static Map randomMap() { - final Map map = new HashMap<>(); - map.put(randomString(), randomString()); - map.put(randomString(), randomString()); - map.put(randomString(), null); - return map; - } - - public static HttpRequestAttachment negativeHttpRequestAttachment() { - return new HttpRequestAttachment( - null, - null, - null, - null, - null, - null, - null - ); - } - -} diff --git a/allure-attachments/src/test/resources/tpl/body-npe-non-safe-attachment.ftl b/allure-attachments/src/test/resources/tpl/body-npe-non-safe-attachment.ftl deleted file mode 100644 index 813425567..000000000 --- a/allure-attachments/src/test/resources/tpl/body-npe-non-safe-attachment.ftl +++ /dev/null @@ -1,36 +0,0 @@ -<#ftl output_format="HTML"> -<#-- @ftlvariable name="data" type="io.qameta.allure.attachment.http.HttpRequestAttachment" --> -
<#if data.method??>${data.method}<#else>GET to <#if data.url??>${data.url}<#else>Unknown
- -

Body

-
-
-    <#t>${data.body.size}
-    
-
- -<#if (data.headers)?has_content> -

Headers

-
- <#list data.headers as name, value> -
${name}: ${value!"null"}
- -
- - - -<#if (data.cookies)?has_content> -

Cookies

-
- <#list data.cookies as name, value> -
${name}: ${value!"null"}
- -
- - -<#if data.curl??> -

Curl

-
- ${data.curl} -
- diff --git a/allure-awaitility/README.md b/allure-awaitility/README.md new file mode 100644 index 000000000..51c1f72f8 --- /dev/null +++ b/allure-awaitility/README.md @@ -0,0 +1,54 @@ +# allure-awaitility + +Awaitility condition listener integration for Allure Java. + +Use this module when your tests wait with Awaitility and you want polling attempts, timeouts, ignored exceptions, and successful waits to be visible in Allure Report. + +## Supported Versions + +- Allure Java 3.x requires Java 17 or newer. +- This module targets Awaitility 4.x. +- The current build validates against Awaitility 4.3.0. + +## Installation + +Gradle: + +```kotlin +dependencies { + testImplementation(platform("io.qameta.allure:allure-bom:")) + testImplementation("io.qameta.allure:allure-awaitility") +} +``` + +Maven, with `allure-bom` imported in dependency management: + +```xml + + io.qameta.allure + allure-awaitility + test + +``` + +## Setup + +Register `io.qameta.allure.awaitility.AllureAwaitilityListener` as an Awaitility condition listener. + +```java +await() + .conditionEvaluationListener(new AllureAwaitilityListener()) + .untilAsserted(() -> assertThat(service.isReady()).isTrue()); +``` + +You can also set it as the default listener for your test suite: + +```java +Awaitility.setDefaultConditionEvaluationListener(new AllureAwaitilityListener()); +``` + +## Report Output + +- Await start, poll, satisfaction, timeout, and exception events. +- Poll attempts as nested Allure steps. +- Timing information rendered through `TemporalDuration`. diff --git a/allure-awaitility/build.gradle.kts b/allure-awaitility/build.gradle.kts index 3c9ed6c79..bbe0f2f5c 100644 --- a/allure-awaitility/build.gradle.kts +++ b/allure-awaitility/build.gradle.kts @@ -8,22 +8,16 @@ dependencies { agent("org.aspectj:aspectjweaver") api(project(":allure-java-commons")) compileOnly("org.awaitility:awaitility:$awaitilityVersion") - testImplementation("javax.annotation:javax.annotation-api") + testImplementation("jakarta.annotation:jakarta.annotation-api") testImplementation("org.assertj:assertj-core") + testImplementation(project(":allure-assertj")) testImplementation("org.awaitility:awaitility:$awaitilityVersion") testImplementation("org.junit.jupiter:junit-jupiter-api") testImplementation("org.slf4j:slf4j-simple") 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.awaitility" - )) - } + testRuntimeOnly("org.junit.platform:junit-platform-launcher") } tasks.test { diff --git a/allure-awaitility/readme.md b/allure-awaitility/readme.md deleted file mode 100644 index 1fcb34691..000000000 --- a/allure-awaitility/readme.md +++ /dev/null @@ -1,71 +0,0 @@ -## Allure-awaitility -Extended logging for poling and ignored exceptions for [awaitility](https://github.com/awaitility/awaitility) - - -## Wiki -For more information about awaitility highly recommended look into [awaitility usage guide](https://github.com/awaitility/awaitility/wiki/Usage) - - -### Configuration examples -Single line for all awaitility conditions in project -```java -Awaitility.setDefaultConditionEvaluationListener(new AllureAwaitilityListener()); -``` - -And another line to prevent breaking allure lifecycle for Steps inside Awaitility evaluations -```java -Awaitility.pollInSameThread(); -``` - -Moreover, it's possible logging only few unstable conditions with method `.conditionEvaluationListener()` -```java -final AtomicInteger atomicInteger = new AtomicInteger(0); -await().with() - .conditionEvaluationListener(new AllureAwaitilityListener()) - .alias("Checking that important counter reached value around 3") - .atMost(Duration.of(1000, ChronoUnit.MILLIS)) - .pollInterval(Duration.of(50, ChronoUnit.MILLIS)) - .until(atomicInteger::getAndIncrement, is(3)); -``` - -How it looks like: -1. Top-level step with condition evaluation definition such as alias or default information. -2. Second-level steps with poling process information -3. Optional second-level step with timeout information -4. Optional second-level steps with ignored information - - -### TimeUnit -Most awaitility users count time as milliseconds, but you can feel free to change print poll information. -```java -Awaitility.setDefaultConditionEvaluationListener(new AllureAwaitilityListener().setUnit(TimeUnit.SECONDS)); -``` - - -### Exceptions handling -By default, it's not possible to handle and log any exceptions, but you can try to -[ignore](https://github.com/awaitility/awaitility/wiki/Usage#ignoring-exceptions) and log ignored exceptions. - -```java -final AtomicInteger atomicInteger = new AtomicInteger(0); -await().with() - .ignoreExceptions() //required - .atMost(Duration.of(1000, ChronoUnit.MILLIS)) - .pollInterval(Duration.of(500, ChronoUnit.MILLIS)) - .until(() -> { - if (atomicInteger.getAndIncrement() != 3) { - //this exception will be ignored by awaitility, but logged into Allure - throw new RuntimeException("Something wrong happens"); - } else { - return true; - } - }); -``` - -Then, if you are not impressed with the large volume of logged exceptions, there is the way to disable logging for -ignored exceptions globally. - -```java -Awaitility.ignoreExceptionsByDefault(); -Awaitility.setDefaultConditionEvaluationListener(new AllureAwaitilityListener().setLogIgnoredExceptions(false)); -``` \ No newline at end of file diff --git a/allure-awaitility/src/main/java/io/qameta/allure/awaitility/AllureAwaitilityListener.java b/allure-awaitility/src/main/java/io/qameta/allure/awaitility/AllureAwaitilityListener.java index 5cd3b2737..d3afd8afb 100644 --- a/allure-awaitility/src/main/java/io/qameta/allure/awaitility/AllureAwaitilityListener.java +++ b/allure-awaitility/src/main/java/io/qameta/allure/awaitility/AllureAwaitilityListener.java @@ -16,7 +16,10 @@ package io.qameta.allure.awaitility; import io.qameta.allure.Allure; +import io.qameta.allure.AllureExternalKey; import io.qameta.allure.AllureLifecycle; +import io.qameta.allure.AllureThreadBinding; +import io.qameta.allure.AttachmentOptions; import io.qameta.allure.model.Status; import io.qameta.allure.model.StepResult; import org.awaitility.Awaitility; @@ -27,11 +30,13 @@ import org.awaitility.core.StartEvaluationEvent; import org.awaitility.core.TimeoutEvent; +import java.io.ByteArrayInputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.nio.charset.StandardCharsets; -import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -57,6 +62,12 @@ * * *

+ *

+ * A listener instance tracks a single condition at a time, so prefer one instance per condition when tests run in + * parallel. Awaitility fires no callback when a non-ignored exception escapes a condition; such an aborted wait + * stays the reporting parent until the same listener starts its next condition, which finalizes the aborted one, + * or until the test ends. + *

* * @see org.awaitility.core.ConditionEvaluationListener * @see Awaitility#setDefaultConditionEvaluationListener(ConditionEvaluationListener) @@ -66,22 +77,15 @@ @SuppressWarnings("unused") public class AllureAwaitilityListener implements ConditionEvaluationListener { - private TimeUnit unit; - private boolean logIgnoredExceptions; + private final AtomicReference unit = new AtomicReference<>(MILLISECONDS); + private final AtomicBoolean logIgnoredExceptions = new AtomicBoolean(true); private final String onStartStepTextPattern; private final String onSatisfiedStepTextPattern; private final String onAwaitStepTextPattern; private final String onTimeoutStepTextPattern; private final String onExceptionStepTextPattern; - private String currentConditionStepUUID; - - private static final InheritableThreadLocal LIFECYCLE = new InheritableThreadLocal() { - @Override - protected AllureLifecycle initialValue() { - return Allure.getLifecycle(); - } - }; + private final AtomicReference currentCondition = new AtomicReference<>(); /** * Returns the lifecycle. @@ -89,15 +93,13 @@ protected AllureLifecycle initialValue() { * @return the Allure lifecycle used by this integration */ public static AllureLifecycle getLifecycle() { - return LIFECYCLE.get(); + return Allure.getLifecycle(); } /** * Default all args constructor with default params. */ public AllureAwaitilityListener() { - this.unit = MILLISECONDS; - this.logIgnoredExceptions = true; this.onStartStepTextPattern = "Awaitility: %s"; this.onSatisfiedStepTextPattern = "%s after %d %s (remaining time %d %s, last poll interval was %s)"; this.onAwaitStepTextPattern = "%s (elapsed time %d %s, remaining time %d %s (last poll interval was %s))"; @@ -112,7 +114,7 @@ public AllureAwaitilityListener() { * @return this factory */ public AllureAwaitilityListener setUnit(final TimeUnit unit) { - this.unit = unit; + this.unit.set(unit); return this; } @@ -123,7 +125,7 @@ public AllureAwaitilityListener setUnit(final TimeUnit unit) { * @return this factory */ public AllureAwaitilityListener setLogIgnoredExceptions(final boolean logging) { - this.logIgnoredExceptions = logging; + this.logIgnoredExceptions.set(logging); return this; } @@ -134,17 +136,27 @@ public AllureAwaitilityListener setLogIgnoredExceptions(final boolean logging) { */ @Override public void beforeEvaluation(final StartEvaluationEvent startEvaluationEvent) { - currentConditionStepUUID = UUID.randomUUID().toString(); - final String nameWoAlias = String.format(onStartStepTextPattern, startEvaluationEvent.getDescription()); - final String nameWithAlias = String.format(onStartStepTextPattern, startEvaluationEvent.getAlias()); - final String stepName = startEvaluationEvent.getAlias() != null ? nameWithAlias : nameWoAlias; - getLifecycle().startStep( - currentConditionStepUUID, - new StepResult() - .setName(stepName) - .setDescription("Awaitility condition started") - .setStatus(Status.FAILED) - ); + finishCurrentCondition(); + final AllureLifecycle lifecycle = getLifecycle(); + lifecycle.getCurrentExecutableKey().ifPresent(parent -> { + final String nameWoAlias = String.format(onStartStepTextPattern, startEvaluationEvent.getDescription()); + final String nameWithAlias = String.format(onStartStepTextPattern, startEvaluationEvent.getAlias()); + final String stepName = startEvaluationEvent.getAlias() != null ? nameWithAlias : nameWoAlias; + final AllureExternalKey conditionStepKey = AllureExternalKey.random(AllureAwaitilityListener.class); + lifecycle.startStep( + parent, + conditionStepKey, + new StepResult() + .setName(stepName) + .setDescription("Awaitility condition started") + .setStatus(Status.FAILED) + ); + final ConditionState condition = new ConditionState(lifecycle, conditionStepKey); + currentCondition.set(condition); + // Keep condition-body steps under the wait. The binding remembers the caller thread's stack, so + // Awaitility may close it from a polling callback without mutating a reused worker's unrelated context. + condition.setBinding(lifecycle.bindDetached(conditionStepKey)); + }); } /** @@ -154,19 +166,18 @@ public void beforeEvaluation(final StartEvaluationEvent startEvaluationE */ @Override public void onTimeout(final TimeoutEvent timeoutEvent) { - getLifecycle().updateStep(awaitilityCondition -> { - final String currentTimeoutStepUUID = UUID.randomUUID().toString(); - getLifecycle().startStep( - currentConditionStepUUID, - currentTimeoutStepUUID, - new StepResult() - .setName(String.format(onTimeoutStepTextPattern, timeoutEvent.getDescription())) - .setDescription("Awaitility condition timeout") - .setStatus(Status.BROKEN) - ); - getLifecycle().stopStep(currentTimeoutStepUUID); - }); - getLifecycle().stopStep(currentConditionStepUUID); + final ConditionState condition = currentCondition.get(); + if (condition == null) { + return; + } + condition.getLifecycle().logStep( + condition.getStepKey(), + new StepResult() + .setName(String.format(onTimeoutStepTextPattern, timeoutEvent.getDescription())) + .setDescription("Awaitility condition timeout") + .setStatus(Status.BROKEN) + ); + finishCondition(condition); } /** @@ -176,10 +187,11 @@ public void onTimeout(final TimeoutEvent timeoutEvent) { */ @Override public void conditionEvaluated(final EvaluatedCondition condition) { + final TimeUnit currentUnit = unit.get(); final String description = condition.getDescription(); - final long elapsedTime = unit.convert(condition.getElapsedTimeInMS(), MILLISECONDS); - final long remainingTime = unit.convert(condition.getRemainingTimeInMS(), MILLISECONDS); - final String unitAsString = unit.toString().toLowerCase(); + final long elapsedTime = currentUnit.convert(condition.getElapsedTimeInMS(), MILLISECONDS); + final long remainingTime = currentUnit.convert(condition.getRemainingTimeInMS(), MILLISECONDS); + final String unitAsString = currentUnit.toString().toLowerCase(); final String message = String.format( condition.isSatisfied() ? onSatisfiedStepTextPattern : onAwaitStepTextPattern, @@ -191,22 +203,80 @@ public void conditionEvaluated(final EvaluatedCondition condition) { new TemporalDuration(condition.getPollInterval()) ); - getLifecycle().updateStep(awaitilityCondition -> { - final String lastAwaitStepUUID = UUID.randomUUID().toString(); - getLifecycle().startStep( - currentConditionStepUUID, - lastAwaitStepUUID, - new StepResult() - .setName(message) - .setDescription("Awaitility condition satisfied or not, but awaiting still in progress") - .setStatus(Status.PASSED) + final ConditionState current = currentCondition.get(); + if (current == null) { + return; + } + current.getLifecycle().logStep( + current.getStepKey(), + new StepResult() + .setName(message) + .setDescription("Awaitility condition satisfied or not, but awaiting still in progress") + .setStatus(Status.PASSED) + ); + if (condition.isSatisfied()) { + current.getLifecycle().updateStep( + current.getStepKey(), awaitilityCondition -> awaitilityCondition.setStatus(Status.PASSED) ); - getLifecycle().stopStep(lastAwaitStepUUID); - if (condition.isSatisfied()) { - awaitilityCondition.setStatus(Status.PASSED); - getLifecycle().stopStep(currentConditionStepUUID); + finishCondition(current); + } + } + + private void finishCurrentCondition() { + final ConditionState condition = currentCondition.getAndSet(null); + if (condition != null) { + condition.finish(); + } + } + + private void finishCondition(final ConditionState condition) { + currentCondition.compareAndSet(condition, null); + condition.finish(); + } + + private static final class ConditionState { + + private final AllureLifecycle lifecycle; + private final AllureExternalKey stepKey; + private final AtomicReference binding = new AtomicReference<>(); + private final AtomicBoolean finished = new AtomicBoolean(); + + private ConditionState(final AllureLifecycle lifecycle, final AllureExternalKey stepKey) { + this.lifecycle = lifecycle; + this.stepKey = stepKey; + } + + private AllureLifecycle getLifecycle() { + return lifecycle; + } + + private AllureExternalKey getStepKey() { + return stepKey; + } + + private void setBinding(final AllureThreadBinding value) { + binding.set(value); + if (finished.get()) { + closeBinding(); } - }); + } + + private void finish() { + if (finished.compareAndSet(false, true)) { + try { + closeBinding(); + } finally { + lifecycle.stopStep(stepKey); + } + } + } + + private void closeBinding() { + final AllureThreadBinding current = binding.getAndSet(null); + if (current != null) { + current.close(); + } + } } /** @@ -224,39 +294,32 @@ public void conditionEvaluated(final EvaluatedCondition condition) { */ @Override public void exceptionIgnored(final IgnoredException ignoredException) { - if (logIgnoredExceptions) { - getLifecycle().updateStep(awaitilityCondition -> { - final String currentExceptionIgnoredStepUUID = UUID.randomUUID().toString(); - final String message = String.format( - onExceptionStepTextPattern, ignoredException.getThrowable().getMessage() - ); - final StringWriter stringWriter = new StringWriter(); - ignoredException.getThrowable().printStackTrace(new PrintWriter(stringWriter)); - final String stackTrace = stringWriter.toString(); - getLifecycle().startStep( - currentConditionStepUUID, - currentExceptionIgnoredStepUUID, - new StepResult() - .setName(message) - .setDescription("Exception occurred and ignored, but awaiting still in progress") - .setStatus(Status.SKIPPED) - ); - getLifecycle().addAttachment( - ignoredException.getThrowable().getMessage(), "text/plain", ".txt", - stackTrace.getBytes(StandardCharsets.UTF_8) - ); - getLifecycle().stopStep(currentExceptionIgnoredStepUUID); - }); + final ConditionState condition = currentCondition.get(); + if (logIgnoredExceptions.get() && condition != null) { + final AllureExternalKey exceptionIgnoredStepKey = AllureExternalKey.random(AllureAwaitilityListener.class); + final String message = String.format( + onExceptionStepTextPattern, ignoredException.getThrowable().getMessage() + ); + final StringWriter stringWriter = new StringWriter(); + ignoredException.getThrowable().printStackTrace(new PrintWriter(stringWriter)); + final String stackTrace = stringWriter.toString(); + condition.getLifecycle().startStep( + condition.getStepKey(), + exceptionIgnoredStepKey, + new StepResult() + .setName(message) + .setDescription("Exception occurred and ignored, but awaiting still in progress") + .setStatus(Status.SKIPPED) + ); + condition.getLifecycle().addAttachment( + exceptionIgnoredStepKey, + ignoredException.getThrowable().getMessage(), + "text/plain", + new ByteArrayInputStream(stackTrace.getBytes(StandardCharsets.UTF_8)), + AttachmentOptions.empty() + ); + condition.getLifecycle().stopStep(exceptionIgnoredStepKey); } } - /** - * For tests only. - * - * @param allure allure lifecycle to set - */ - public static void setLifecycle(final AllureLifecycle allure) { - LIFECYCLE.set(allure); - } - } diff --git a/allure-awaitility/src/main/java/module-info.java b/allure-awaitility/src/main/java/module-info.java new file mode 100644 index 000000000..e9a7968db --- /dev/null +++ b/allure-awaitility/src/main/java/module-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ +module io.qameta.allure.awaitility { + requires transitive io.qameta.allure.commons; + requires awaitility; + + exports io.qameta.allure.awaitility; +} diff --git a/allure-awaitility/src/test/java/io/qameta/allure/awaitility/ConditionListenersPositiveTest.java b/allure-awaitility/src/test/java/io/qameta/allure/awaitility/ConditionListenersPositiveTest.java index 9eb6d7d6f..50f367968 100644 --- a/allure-awaitility/src/test/java/io/qameta/allure/awaitility/ConditionListenersPositiveTest.java +++ b/allure-awaitility/src/test/java/io/qameta/allure/awaitility/ConditionListenersPositiveTest.java @@ -15,167 +15,305 @@ */ package io.qameta.allure.awaitility; +import io.qameta.allure.Description; import io.qameta.allure.model.Status; import io.qameta.allure.model.StepResult; import io.qameta.allure.model.TestResult; +import io.qameta.allure.test.IsolatedLifecycle; import org.awaitility.Awaitility; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.DynamicNode; -import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestFactory; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Stream; import static io.qameta.allure.test.RunUtils.runWithinTestContext; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.DynamicTest.dynamicTest; +@IsolatedLifecycle class ConditionListenersPositiveTest { + private static final String AWAITILITY_EVALUATION_DESCRIPTION = "Awaitility condition satisfied or not, but awaiting still in progress"; + @BeforeAll static void setup() { Awaitility.pollInSameThread(); } /** - * Positive test to check proper allure steps generation. - *

- * Precondition: listener into condition declaration, await without alias - *

- * Test should check that: - *

  • 1. Allure has exactly 1 top-level step for 1 await condition
  • - *
  • 2. Top level step has passed status
  • - *
  • 3. Top level step has default name
  • + * Verifies that a successful Awaitility condition with an inline Allure listener is reported as one top-level step + * when no await alias is provided. */ - @TestFactory - Stream globalSettingsAwaitWoAliasCheckTopLevelPassedStep() { - final List testResult = runWithinTestContext(() -> { - final AtomicInteger atomicInteger = new AtomicInteger(0); - await().with() + @Description + @Test + void awaitWithoutAliasShouldCreateSingleTopLevelStep() { + final List steps = runAwaitWithoutAliasTopLevelSteps(); + + assertThat(steps) + .as("Exactly 1 top level step for 1 awaitility condition") + .hasSize(1); + } + + /** + * Verifies that the top-level report step for a successful Awaitility condition with an inline listener is marked + * passed. + */ + @Description + @Test + void awaitWithoutAliasShouldMarkTopLevelStepPassed() { + final StepResult step = awaitWithoutAliasTopLevelStep(); + + assertThat(step.getStatus()) + .as("Top level step has passed status") + .isEqualTo(Status.PASSED); + } + + /** + * Verifies that a successful Awaitility condition without an alias keeps the default top-level step name in the + * Allure report. + */ + @Description + @Test + void awaitWithoutAliasShouldUseDefaultTopLevelStepName() { + final StepResult step = awaitWithoutAliasTopLevelStep(); + + assertThat(step.getName()) + .as("Top level step has default name because await() wo alias") + .isEqualTo("Awaitility: Starting evaluation"); + } + + /** + * Verifies that an Awaitility alias supplied on a condition with an inline Allure listener is visible in the + * top-level report step name. + */ + @Description + @Test + void globalSettingsAwaitWithAliasCheckTopLevelPassedStep() { + final StepResult step = awaitWithAliasTopLevelStep(); + + assertThat(step.getName()) + .as("Top level step has name with alias") + .isEqualTo("Awaitility: Counter should be at least 3"); + } + + /** + * Verifies that report consumers can see one condition-evaluation child step for each poll performed before a + * successful Awaitility condition completes. + */ + @Description + @Test + void awaitWithoutAliasShouldCreateSecondLevelStepForEachPoll() { + final List steps = awaitWithoutAliasPollSteps(); + + assertThat(steps) + .as("Exactly 4 second level steps for 4 polling iterations") + .hasSize(4); + } + + /** + * Verifies that all poll-level report steps for a successfully completed Awaitility condition are marked passed. + */ + @Description + @Test + void awaitWithoutAliasShouldMarkAllSecondLevelStepsPassed() { + final List steps = awaitWithoutAliasPollSteps(); + + assertThat(steps) + .as("All second level steps has passed statuses") + .allMatch(x -> x.getStatus().equals(Status.PASSED)); + } + + /** + * Verifies that the first failed poll records the evaluated condition, expected value, actual value, and timing + * context in the report. + */ + @Description + @Test + void awaitWithoutAliasShouldDescribeFirstFailedPoll() { + final StepResult step = firstFailedPollStep(); + + assertThat(step.getName()) + .contains("io.qameta.allure.awaitility.ConditionListenersPositiveTest") + .contains("expected: 3") + .contains("but was: 0") + .contains("elapsed time") + .contains("remaining time") + .contains("last poll interval was"); + } + + /** + * Verifies that the second failed poll records the next observed value together with the evaluated condition and + * timing context. + */ + @Description + @Test + void awaitWithoutAliasShouldDescribeSecondFailedPoll() { + final StepResult step = secondFailedPollStep(); + + assertThat(step.getName()) + .contains("io.qameta.allure.awaitility.ConditionListenersPositiveTest") + .contains("expected: 3") + .contains("but was: 1") + .contains("elapsed time") + .contains("remaining time") + .contains("last poll interval was"); + } + + /** + * Verifies that the third failed poll records the final unsuccessful value before the Awaitility condition succeeds. + */ + @Description + @Test + void awaitWithoutAliasShouldDescribeThirdFailedPoll() { + final StepResult step = thirdFailedPollStep(); + + assertThat(step.getName()) + .contains("io.qameta.allure.awaitility.ConditionListenersPositiveTest") + .contains("expected: 3") + .contains("but was: 2") + .contains("elapsed time") + .contains("remaining time") + .contains("last poll interval was"); + } + + /** + * Verifies that the successful poll reports the reached value and timing context so consumers can identify why the + * wait completed. + */ + @Description + @Test + void awaitWithoutAliasShouldDescribeSuccessfulPoll() { + final StepResult step = successfulPollStep(); + + assertThat(step.getName()) + .contains("io.qameta.allure.awaitility.ConditionListenersPositiveTest") + .contains("reached its end value after") + .contains("remaining time") + .contains("last poll interval was"); + } + + /** + * Verifies that Awaitility 4.3's supplier-and-consumer {@code untilAsserted} overload reports every supplied value + * under one successful Awaitility step. + */ + @Description + @Test + void supplierConsumerUntilAssertedShouldReportPollsUnderSingleAwaitilityStep() { + final List testResults = runWithinTestContext(() -> { + final AtomicInteger counter = new AtomicInteger(); + await("supplied counter reaches 2").with() .conditionEvaluationListener(new AllureAwaitilityListener()) .atMost(Duration.of(1000, ChronoUnit.MILLIS)) - .pollInterval(Duration.of(50, ChronoUnit.MILLIS)) - .until(atomicInteger::getAndIncrement, is(3)); - }, - AllureAwaitilityListener::setLifecycle - ).getTestResults(); + .pollInterval(Duration.of(10, ChronoUnit.MILLIS)) + .untilAsserted(counter::getAndIncrement, value -> assertThat(value).isEqualTo(2)); + }).getTestResults(); - return Stream.of( - DynamicTest.dynamicTest( - "Exactly 1 top level step for 1 awaitility condition", () -> assertThat(testResult.get(0).getSteps()) - .hasSize(1) - ), - DynamicTest.dynamicTest( - "Top level step has passed status", () -> assertThat(testResult.get(0).getSteps()) - .allMatch(step -> Status.PASSED.equals(step.getStatus())) - ), - DynamicTest.dynamicTest( - "Top level step has default name because await() wo alias", () -> assertThat(testResult.get(0).getSteps()) - .extracting(StepResult::getName) - .containsExactly("Awaitility: Starting evaluation") - ) - ); + final TestResult testResult = testResults.get(0); + assertThat(testResult.getSteps()) + .singleElement() + .satisfies(step -> { + assertThat(step.getName()).isEqualTo("Awaitility: supplied counter reaches 2"); + assertThat(step.getStatus()).isEqualTo(Status.PASSED); + final List evaluationSteps = step.getSteps().stream() + .filter(ConditionListenersPositiveTest::isAwaitilityEvaluationStep) + .toList(); + assertThat(evaluationSteps) + .hasSize(3); + }); } /** - * Positive test to check proper allure steps generation. - *

    - * Precondition: listener into condition declaration, await with alias - *

    - * Test should check that: - *

  • 1. Top level step has name with alias from await('alias')
  • + * Verifies that a nested wait with its own listener keeps the outer condition active and reports the inner + * condition beneath it. */ + @Description @Test - void globalSettingsAwaitWithAliasCheckTopLevelPassedStep() { + void nestedAwaitShouldKeepOuterConditionScope() { + final List testResults = runWithinTestContext( + () -> await("outer condition").with() + .pollInSameThread() + .conditionEvaluationListener(new AllureAwaitilityListener()) + .atMost(Duration.ofSeconds(1)) + .until(() -> { + await("inner condition").with() + .pollInSameThread() + .conditionEvaluationListener(new AllureAwaitilityListener()) + .atMost(Duration.ofSeconds(1)) + .until(() -> true); + return true; + }) + ).getTestResults(); + + final List topLevelSteps = testResults.get(0).getSteps(); + assertThat(topLevelSteps) + .extracting(StepResult::getName) + .containsExactly("Awaitility: outer condition"); + + final List nestedStepNames = topLevelSteps.get(0).getSteps().stream() + .map(StepResult::getName) + .toList(); + assertThat(nestedStepNames) + .as("nested condition step names") + .contains("Awaitility: inner condition"); + } + + private List runAwaitWithoutAliasTopLevelSteps() { final List testResult = runWithinTestContext(() -> { final AtomicInteger atomicInteger = new AtomicInteger(0); - await("Counter should be at least 3").with() + await().with() .conditionEvaluationListener(new AllureAwaitilityListener()) .atMost(Duration.of(1000, ChronoUnit.MILLIS)) .pollInterval(Duration.of(50, ChronoUnit.MILLIS)) - .until(atomicInteger::getAndIncrement, is(3)); - }, - AllureAwaitilityListener::setLifecycle - ).getTestResults(); - assertEquals( - "Awaitility: Counter should be at least 3", - testResult.get(0).getSteps().get(0).getName(), - "Top level step has name with alias" - ); + .untilAsserted(() -> assertThat(atomicInteger.getAndIncrement()).isEqualTo(3)); + }).getTestResults(); + + return testResult.get(0).getSteps(); } - /** - * Positive test to check proper allure steps generation. - *

    - * Precondition: listener into condition declaration, await without alias - *

    - * Test should check that: - *

  • 1. Allure has exactly 4 second-level steps for condition with 4 polls iteration
  • - *
  • 2. All second-level steps should have passed status for successful condition evaluation
  • - *
  • 3. All second-level steps should have information about polling intervals and evaluation
  • - */ - @TestFactory - Stream globalSettingsCheckAwaitWoAliasSecondLevelPassedSteps() { + private StepResult awaitWithoutAliasTopLevelStep() { + return runAwaitWithoutAliasTopLevelSteps().get(0); + } + + private StepResult awaitWithAliasTopLevelStep() { final List testResult = runWithinTestContext(() -> { final AtomicInteger atomicInteger = new AtomicInteger(0); - await().with() + await("Counter should be at least 3").with() .conditionEvaluationListener(new AllureAwaitilityListener()) .atMost(Duration.of(1000, ChronoUnit.MILLIS)) .pollInterval(Duration.of(50, ChronoUnit.MILLIS)) - .until(atomicInteger::getAndIncrement, is(3)); - }, - AllureAwaitilityListener::setLifecycle - ).getTestResults(); + .untilAsserted(() -> assertThat(atomicInteger.getAndIncrement()).isEqualTo(3)); + }).getTestResults(); + + return testResult.get(0).getSteps().get(0); + } + + private List awaitWithoutAliasPollSteps() { + return awaitWithoutAliasTopLevelStep().getSteps().stream() + .filter(ConditionListenersPositiveTest::isAwaitilityEvaluationStep) + .toList(); + } + + private static boolean isAwaitilityEvaluationStep(final StepResult step) { + return AWAITILITY_EVALUATION_DESCRIPTION.equals(step.getDescription()); + } + + private StepResult firstFailedPollStep() { + return awaitWithoutAliasPollSteps().get(0); + } + + private StepResult secondFailedPollStep() { + return awaitWithoutAliasPollSteps().get(1); + } + + private StepResult thirdFailedPollStep() { + return awaitWithoutAliasPollSteps().get(2); + } - return Stream.of( - dynamicTest( - "Exactly 4 second level steps for 4 polling iterations", () -> assertThat(testResult.get(0).getSteps().get(0).getSteps()) - .hasSize(4) - ), - dynamicTest( - "All second level steps has passed statuses", () -> assertThat(testResult.get(0).getSteps().get(0).getSteps()) - .allMatch(x -> x.getStatus().equals(Status.PASSED)) - ), - dynamicTest( - "Second level step 1 name", () -> assertThat(testResult.get(0).getSteps().get(0).getSteps().get(0).getName()) - .contains("io.qameta.allure.awaitility.ConditionListenersPositiveTest") - .contains("expected <3> but was <0>") - .contains("elapsed time") - .contains("remaining time") - .contains("last poll interval was") - ), - dynamicTest( - "Second level step 2 name", () -> assertThat(testResult.get(0).getSteps().get(0).getSteps().get(1).getName()) - .contains("io.qameta.allure.awaitility.ConditionListenersPositiveTest") - .contains("expected <3> but was <1>") - .contains("elapsed time") - .contains("remaining time") - .contains("last poll interval was") - ), - dynamicTest( - "Second level step 3 name", () -> assertThat(testResult.get(0).getSteps().get(0).getSteps().get(2).getName()) - .contains("io.qameta.allure.awaitility.ConditionListenersPositiveTest") - .contains("expected <3> but was <2>") - .contains("elapsed time") - .contains("remaining time") - .contains("last poll interval was") - ), - dynamicTest( - "Second level step 4 name", () -> assertThat(testResult.get(0).getSteps().get(0).getSteps().get(3).getName()) - .contains("io.qameta.allure.awaitility.ConditionListenersPositiveTest") - .contains("reached its end value of <3> after") - .contains("remaining time") - .contains("last poll interval was") - ) - ); + private StepResult successfulPollStep() { + return awaitWithoutAliasPollSteps().get(3); } } diff --git a/allure-awaitility/src/test/java/io/qameta/allure/awaitility/ExceptionReportingTest.java b/allure-awaitility/src/test/java/io/qameta/allure/awaitility/ExceptionReportingTest.java new file mode 100644 index 000000000..210e09503 --- /dev/null +++ b/allure-awaitility/src/test/java/io/qameta/allure/awaitility/ExceptionReportingTest.java @@ -0,0 +1,92 @@ +/* + * 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.awaitility; + +import io.qameta.allure.Allure; +import io.qameta.allure.Description; +import io.qameta.allure.model.Stage; +import io.qameta.allure.model.StepResult; +import io.qameta.allure.model.TestResult; +import io.qameta.allure.test.IsolatedLifecycle; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import static io.qameta.allure.test.RunUtils.runWithinTestContext; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +@IsolatedLifecycle +class ExceptionReportingTest { + + /** + * Verifies that reusing a listener after a propagated condition exception cannot retain the failed wait as the + * parent of the next condition or later test steps, and that the aborted wait's step is finalized when the next + * condition starts. Awaitility fires no callback for a propagated exception, so a wait aborted this way is + * cleaned up when the same listener starts its next condition. + */ + @Description + @Test + void shouldDiscardAbortedConditionBindingBeforeListenerReuse() { + final IllegalStateException failure = new IllegalStateException("first condition aborted"); + final AtomicReference caught = new AtomicReference<>(); + final AllureAwaitilityListener listener = new AllureAwaitilityListener(); + + final List testResults = runWithinTestContext(() -> { + try { + await("aborting condition").with() + .pollInSameThread() + .conditionEvaluationListener(listener) + .atMost(Duration.ofSeconds(1)) + .until(() -> { + throw failure; + }); + } catch (IllegalStateException exception) { + caught.set(exception); + } + + await("following condition").with() + .pollInSameThread() + .conditionEvaluationListener(listener) + .atMost(Duration.ofSeconds(1)) + .until(() -> true); + Allure.step("step after following condition"); + }).getTestResults(); + + assertThat(caught.get()) + .isSameAs(failure); + + final List topLevelSteps = testResults.get(0).getSteps(); + assertThat(topLevelSteps) + .extracting(StepResult::getName) + .containsExactly( + "Awaitility: aborting condition", + "Awaitility: following condition", + "step after following condition" + ); + + final StepResult abortedCondition = topLevelSteps.get(0); + assertThat(abortedCondition.getStage()) + .as("aborted condition stage") + .isEqualTo(Stage.FINISHED); + assertThat(abortedCondition.getStop()) + .as("aborted condition stop time") + .isGreaterThanOrEqualTo(abortedCondition.getStart()); + } + +} diff --git a/allure-awaitility/src/test/java/io/qameta/allure/awaitility/ExecutorServiceReportingTest.java b/allure-awaitility/src/test/java/io/qameta/allure/awaitility/ExecutorServiceReportingTest.java new file mode 100644 index 000000000..e22e2352f --- /dev/null +++ b/allure-awaitility/src/test/java/io/qameta/allure/awaitility/ExecutorServiceReportingTest.java @@ -0,0 +1,86 @@ +/* + * 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.awaitility; + +import io.qameta.allure.Allure; +import io.qameta.allure.Description; +import io.qameta.allure.Issue; +import io.qameta.allure.model.StepResult; +import io.qameta.allure.model.TestResult; +import io.qameta.allure.test.IsolatedLifecycle; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static io.qameta.allure.test.RunUtils.runWithinTestContext; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +@IsolatedLifecycle +class ExecutorServiceReportingTest { + + /** + * Verifies that reusing an Awaitility polling thread cannot retain a finished wait as the parent of steps produced + * later by the test, protecting the report hierarchy regression described in issue 891. + */ + @Description + @Issue("891") + @Test + void shouldKeepFollowingStepsAtTestLevelWhenExecutorIsReused() throws Exception { + final ExecutorService executor = Executors.newSingleThreadExecutor(); + try { + executor.submit(() -> { + // Start the reusable worker before an Allure test context exists. + }).get(5, SECONDS); + + final AllureAwaitilityListener listener = new AllureAwaitilityListener(); + final List testResults = runWithinTestContext(() -> { + await("first condition").with() + .pollExecutorService(executor) + .conditionEvaluationListener(listener) + .atMost(Duration.ofSeconds(1)) + .until(() -> true); + Allure.step("step after first condition"); + + await("second condition").with() + .pollExecutorService(executor) + .conditionEvaluationListener(listener) + .atMost(Duration.ofSeconds(1)) + .until(() -> true); + Allure.step("step after second condition"); + }).getTestResults(); + + final TestResult testResult = testResults.get(0); + final List topLevelSteps = testResult.getSteps(); + + assertThat(topLevelSteps) + .extracting(StepResult::getName) + .containsExactly( + "Awaitility: first condition", + "step after first condition", + "Awaitility: second condition", + "step after second condition" + ); + } finally { + executor.shutdownNow(); + } + } + +} diff --git a/allure-awaitility/src/test/java/io/qameta/allure/awaitility/GlobalSettingsNegativeTest.java b/allure-awaitility/src/test/java/io/qameta/allure/awaitility/GlobalSettingsNegativeTest.java index c00fcf774..61d5f3ff8 100644 --- a/allure-awaitility/src/test/java/io/qameta/allure/awaitility/GlobalSettingsNegativeTest.java +++ b/allure-awaitility/src/test/java/io/qameta/allure/awaitility/GlobalSettingsNegativeTest.java @@ -15,32 +15,34 @@ */ package io.qameta.allure.awaitility; +import io.qameta.allure.Description; import io.qameta.allure.model.Status; +import io.qameta.allure.model.StepResult; import io.qameta.allure.model.TestResult; +import io.qameta.allure.test.IsolatedLifecycle; import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DynamicNode; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestFactory; import org.junit.jupiter.api.TestInstance; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Stream; import static io.qameta.allure.test.RunUtils.runWithinTestContext; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.DynamicTest.dynamicTest; @TestInstance(TestInstance.Lifecycle.PER_METHOD) +@IsolatedLifecycle class GlobalSettingsNegativeTest { + private static final String AWAITILITY_EVALUATION_DESCRIPTION = "Awaitility condition satisfied or not, but awaiting still in progress"; + + private static final String AWAITILITY_TIMEOUT_DESCRIPTION = "Awaitility condition timeout"; + @AfterEach void reset() { Awaitility.reset(); @@ -53,80 +55,123 @@ void setup() { } /** - * Negative test to check proper allure steps generation. - *

    - * Precondition: static settings, await without alias - *

    - * Test should check that: - *

  • 1. Top level step has broken status
  • + * Verifies that a timed-out Awaitility condition using the global Allure listener marks the top-level wait step as + * failed. */ + @Description @Test - void globalSettingsAwaitWoAliasCheckTopLevelBrokenStep() { - final List testResult = runWithinTestContext(() -> { - final AtomicInteger atomicInteger = new AtomicInteger(0); - await().with() - .atMost(Duration.of(1000, ChronoUnit.MILLIS)) - .pollInterval(Duration.of(500, ChronoUnit.MILLIS)) - .until(atomicInteger::getAndIncrement, is(3)); - }, - AllureAwaitilityListener::setLifecycle - ).getTestResults(); - assertEquals( - Status.FAILED, testResult.get(0).getSteps().get(0).getStatus(), - "Top level step has broken status" - ); + void globalSettingsAwaitWoAliasCheckTopLevelFailedStep() { + final StepResult step = timedOutAwaitTopLevelStep(); + + assertThat(step.getStatus()) + .as("Top level step has failed status") + .isEqualTo(Status.FAILED); + } + + /** + * Verifies that a timed-out Awaitility condition exposes both the failed condition-evaluation step and the timeout + * event as child report steps. + */ + @Description + @Test + void timedOutAwaitShouldCreateSecondLevelStepForFailedPollAndTimeout() { + final List steps = timedOutAwaitPollSteps(); + + assertThat(steps) + .as("Exactly 2 second level steps for 2 polling iterations") + .hasSize(2); + } + + /** + * Verifies that the failed poll before timeout records the evaluated condition, expected and actual values, and + * timing context. + */ + @Description + @Test + void timedOutAwaitShouldDescribeFailedPoll() { + final StepResult step = failedPollStepBeforeTimeout(); + + assertThat(step.getName()) + .contains("io.qameta.allure.awaitility.GlobalSettingsNegativeTest") + .contains("expected: 3") + .contains("but was: 0") + .contains("elapsed time") + .contains("remaining time") + .contains("last poll interval was"); } /** - * Positive test to check proper allure steps generation. - *

    - * Precondition: static settings, await without alias - *

    - * Test should check that: - *

  • 1. Allure has exactly 2 second-level steps for condition with 2 polls iteration
  • - *
  • 2. All second-level steps should have passed or broken status for successful and timeout evaluations
  • - *
  • 3. All second-level steps should have information about polling intervals and evaluation
  • + * Verifies that a failed poll that has not yet exhausted the wait timeout remains passed in the report. */ - @TestFactory - Stream globalSettingsCheckAwaitWoAliasSecondLevelTimeoutStep() { + @Description + @Test + void timedOutAwaitShouldMarkFailedPollPassed() { + final StepResult step = failedPollStepBeforeTimeout(); + + assertThat(step.getStatus()) + .isEqualTo(Status.PASSED); + } + + /** + * Verifies that the terminal poll for a timed-out wait clearly reports the timeout condition to report consumers. + */ + @Description + @Test + void timedOutAwaitShouldDescribeTimeoutPoll() { + final StepResult step = timeoutPollStep(); + + assertThat(step.getName()) + .contains("Condition timeout.") + .contains("io.qameta.allure.awaitility.GlobalSettingsNegativeTest"); + } + + /** + * Verifies that the terminal timeout poll is marked broken so the report distinguishes timeout failure from normal + * polling attempts. + */ + @Description + @Test + void timedOutAwaitShouldMarkTimeoutPollBroken() { + final StepResult step = timeoutPollStep(); + + assertThat(step.getStatus()) + .isEqualTo(Status.BROKEN); + } + + private List runTimedOutAwaitWithoutAliasTopLevelSteps() { final List testResult = runWithinTestContext(() -> { final AtomicInteger atomicInteger = new AtomicInteger(0); await().with() .atMost(Duration.of(1000, ChronoUnit.MILLIS)) .pollInterval(Duration.of(500, ChronoUnit.MILLIS)) - .until(atomicInteger::getAndIncrement, is(3)); - }, - AllureAwaitilityListener::setLifecycle + .untilAsserted(() -> assertThat(atomicInteger.getAndIncrement()).isEqualTo(3)); + } ).getTestResults(); - return Stream.of( - dynamicTest( - "Second level steps count", () -> assertThat(testResult.get(0).getSteps().get(0).getSteps()) - .as("Exactly 2 second level steps for 2 polling iterations") - .hasSize(2) - ), - dynamicTest( - "Second level step 1 name", () -> assertThat(testResult.get(0).getSteps().get(0).getSteps().get(0).getName()) - .contains("io.qameta.allure.awaitility.GlobalSettingsNegativeTest") - .contains("expected <3> but was <0>") - .contains("elapsed time") - .contains("remaining time") - .contains("last poll interval was") - ), - dynamicTest( - "Second level step 1 status", () -> assertThat(testResult.get(0).getSteps().get(0).getSteps().get(0).getStatus()) - .isEqualTo(Status.PASSED) - ), - dynamicTest( - "Second level step 2 name", () -> assertThat(testResult.get(0).getSteps().get(0).getSteps().get(1).getName()) - .contains("Condition timeout.") - .contains("io.qameta.allure.awaitility.GlobalSettingsNegativeTest") - ), - dynamicTest( - "Second level step 2 status", () -> assertThat(testResult.get(0).getSteps().get(0).getSteps().get(1).getStatus()) - .isEqualTo(Status.BROKEN) - ) - ); + return testResult.get(0).getSteps(); + } + + private StepResult timedOutAwaitTopLevelStep() { + return runTimedOutAwaitWithoutAliasTopLevelSteps().get(0); + } + + private List timedOutAwaitPollSteps() { + return timedOutAwaitTopLevelStep().getSteps().stream() + .filter(GlobalSettingsNegativeTest::isAwaitilityEvaluationOrTimeoutStep) + .toList(); + } + + private static boolean isAwaitilityEvaluationOrTimeoutStep(final StepResult step) { + return AWAITILITY_EVALUATION_DESCRIPTION.equals(step.getDescription()) + || AWAITILITY_TIMEOUT_DESCRIPTION.equals(step.getDescription()); + } + + private StepResult failedPollStepBeforeTimeout() { + return timedOutAwaitPollSteps().get(0); + } + + private StepResult timeoutPollStep() { + return timedOutAwaitPollSteps().get(1); } } diff --git a/allure-awaitility/src/test/java/io/qameta/allure/awaitility/GlobalSettingsPositiveTest.java b/allure-awaitility/src/test/java/io/qameta/allure/awaitility/GlobalSettingsPositiveTest.java index dfb3ff3b7..0a89f8ea5 100644 --- a/allure-awaitility/src/test/java/io/qameta/allure/awaitility/GlobalSettingsPositiveTest.java +++ b/allure-awaitility/src/test/java/io/qameta/allure/awaitility/GlobalSettingsPositiveTest.java @@ -15,13 +15,14 @@ */ package io.qameta.allure.awaitility; +import io.qameta.allure.Description; import io.qameta.allure.model.Status; import io.qameta.allure.model.StepResult; import io.qameta.allure.model.TestResult; +import io.qameta.allure.test.IsolatedLifecycle; import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DynamicNode; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; @@ -29,18 +30,17 @@ import java.time.temporal.ChronoUnit; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Stream; import static io.qameta.allure.test.RunUtils.runWithinTestContext; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.DynamicTest.dynamicTest; @TestInstance(TestInstance.Lifecycle.PER_METHOD) +@IsolatedLifecycle class GlobalSettingsPositiveTest { + private static final String AWAITILITY_EVALUATION_DESCRIPTION = "Awaitility condition satisfied or not, but awaiting still in progress"; + @AfterEach void reset() { Awaitility.reset(); @@ -53,147 +53,212 @@ void setup() { } /** - * Positive test to check proper allure steps generation. - *

    - * Precondition: static settings, await without alias - *

    - * Test should check that: - *

  • 1. Allure has exactly 1 top-level step for 1 await condition
  • - *
  • 2. Top level step has passed status
  • - *
  • 3. Top level step has default name
  • + * Verifies that a successful Awaitility condition using the global Allure listener is reported as one top-level step + * when no await alias is provided. */ + @Description @Test - Stream globalSettingsAwaitWoAliasCheckTopLevelPassedStep() { - final List testResult = runWithinTestContext(() -> { - final AtomicInteger atomicInteger = new AtomicInteger(0); - await().with() - .atMost(Duration.of(1000, ChronoUnit.MILLIS)) - .pollInterval(Duration.of(50, ChronoUnit.MILLIS)) - .until(atomicInteger::getAndIncrement, is(3)); - }, - AllureAwaitilityListener::setLifecycle - ).getTestResults(); - - return Stream.of( - dynamicTest( - "Steps count", () -> assertThat(testResult.get(0).getSteps()) - .as("Exactly 1 top level step for 1 awaitility condition") - .hasSize(1) - ), - dynamicTest( - "Top level step status", () -> assertEquals( - Status.PASSED, testResult.get(0).getSteps().get(0).getStatus(), - "Top level step has passed status" - ) - ), - dynamicTest( - "Top level step name", () -> assertEquals( - "Awaitility: Starting evaluation", - testResult.get(0).getSteps().get(0).getName(), - "Top level step has default name because await() wo alias" - ) - ) - ); + void awaitWithoutAliasShouldCreateSingleTopLevelStep() { + final List steps = runAwaitWithoutAliasTopLevelSteps(); + + assertThat(steps) + .as("Exactly 1 top level step for 1 awaitility condition") + .hasSize(1); } /** - * Positive test to check proper allure steps generation. - *

    - * Precondition: static settings, await with alias - *

    - * Test should check that: - *

  • 1. Top level step has name with alias from await('alias')
  • + * Verifies that the top-level report step created by the global Awaitility listener is marked passed for a + * successful condition. */ + @Description + @Test + void awaitWithoutAliasShouldMarkTopLevelStepPassed() { + final StepResult step = awaitWithoutAliasTopLevelStep(); + + assertThat(step.getStatus()) + .as("Top level step has passed status") + .isEqualTo(Status.PASSED); + } + + /** + * Verifies that a successful globally-listened Awaitility condition without an alias keeps the default top-level + * step name in the report. + */ + @Description + @Test + void awaitWithoutAliasShouldUseDefaultTopLevelStepName() { + final StepResult step = awaitWithoutAliasTopLevelStep(); + + assertThat(step.getName()) + .as("Top level step has default name because await() wo alias") + .isEqualTo("Awaitility: Starting evaluation"); + } + + /** + * Verifies that an Awaitility alias is preserved in the top-level report step when the listener is configured + * globally. + */ + @Description @Test void globalSettingsAwaitWithAliasCheckTopLevelPassedStep() { - final List testResult = runWithinTestContext(() -> { - final AtomicInteger atomicInteger = new AtomicInteger(0); - await("Counter should be at least 3").with() - .atMost(Duration.of(1000, ChronoUnit.MILLIS)) - .pollInterval(Duration.of(50, ChronoUnit.MILLIS)) - .until(atomicInteger::getAndIncrement, is(3)); - }, - AllureAwaitilityListener::setLifecycle - ).getTestResults(); - assertEquals( - "Awaitility: Counter should be at least 3", - testResult.get(0).getSteps().get(0).getName(), - "Top level step has name with alias" - ); + final StepResult step = awaitWithAliasTopLevelStep(); + + assertThat(step.getName()) + .as("Top level step has name with alias") + .isEqualTo("Awaitility: Counter should be at least 3"); + } + + /** + * Verifies that the global Awaitility listener creates one condition-evaluation child report step for each poll + * before a successful condition completes. + */ + @Description + @Test + void awaitWithoutAliasShouldCreateSecondLevelStepForEachPoll() { + final List steps = awaitWithoutAliasPollSteps(); + + assertThat(steps) + .as("Exactly 4 second level steps for 4 polling iterations") + .hasSize(4); } /** - * Positive test to check proper allure steps generation. - *

    - * Precondition: static settings, await without alias - *

    - * Test should check that: - *

  • 1. Allure has exactly 4 second-level steps for condition with 4 polls iteration
  • - *
  • 2. All second-level steps should have passed status for successful condition evaluation
  • - *
  • 3. All second-level steps should have information about polling intervals and evaluation
  • + * Verifies that every poll-level report step generated by the global listener is passed for a successful condition. */ + @Description @Test - Stream globalSettingsCheckAwaitWoAliasSecondLevelPassedSteps() { + void awaitWithoutAliasShouldMarkAllSecondLevelStepsPassed() { + final List steps = awaitWithoutAliasPollSteps(); + + assertThat(steps) + .extracting(StepResult::getStatus) + .containsExactlyInAnyOrder( + Status.PASSED, + Status.PASSED, + Status.PASSED, + Status.PASSED + ); + } + + /** + * Verifies that the first failed poll from the global listener reports the evaluated condition, expected and actual + * values, and timing context. + */ + @Description + @Test + void awaitWithoutAliasShouldDescribeFirstFailedPoll() { + final StepResult step = firstFailedPollStep(); + + assertThat(step.getName()) + .contains("io.qameta.allure.awaitility.GlobalSettingsPositiveTest") + .contains("expected: 3") + .contains("but was: 0") + .contains("elapsed time") + .contains("remaining time") + .contains("last poll interval was"); + } + + /** + * Verifies that the second failed poll from the global listener reports the next observed value with timing context. + */ + @Description + @Test + void awaitWithoutAliasShouldDescribeSecondFailedPoll() { + final StepResult step = secondFailedPollStep(); + + assertThat(step.getName()) + .contains("io.qameta.allure.awaitility.GlobalSettingsPositiveTest") + .contains("expected: 3") + .contains("but was: 1") + .contains("elapsed time") + .contains("remaining time") + .contains("last poll interval was"); + } + + /** + * Verifies that the third failed poll from the global listener reports the final unsuccessful value before success. + */ + @Description + @Test + void awaitWithoutAliasShouldDescribeThirdFailedPoll() { + final StepResult step = thirdFailedPollStep(); + + assertThat(step.getName()) + .contains("io.qameta.allure.awaitility.GlobalSettingsPositiveTest") + .contains("expected: 3") + .contains("but was: 2") + .contains("elapsed time") + .contains("remaining time") + .contains("last poll interval was"); + } + + /** + * Verifies that the successful poll from the global listener reports the reached value and timing context. + */ + @Description + @Test + void awaitWithoutAliasShouldDescribeSuccessfulPoll() { + final StepResult step = successfulPollStep(); + + assertThat(step.getName()) + .contains("io.qameta.allure.awaitility.GlobalSettingsPositiveTest") + .contains("reached its end value after") + .contains("remaining time") + .contains("last poll interval was"); + } + + private List runAwaitWithoutAliasTopLevelSteps() { final List testResult = runWithinTestContext(() -> { final AtomicInteger atomicInteger = new AtomicInteger(0); await().with() .atMost(Duration.of(1000, ChronoUnit.MILLIS)) .pollInterval(Duration.of(50, ChronoUnit.MILLIS)) - .until(atomicInteger::getAndIncrement, is(3)); - }, - AllureAwaitilityListener::setLifecycle - ).getTestResults(); - - return Stream.of( - dynamicTest( - "Second level steps count", () -> assertEquals( - 4, testResult.get(0).getSteps().get(0).getSteps().size(), - "Exactly 4 second level steps for 4 polling iterations" - ) - ), - dynamicTest( - "Second level steps all passed", () -> assertThat(testResult.get(0).getSteps().get(0).getSteps()) - .extracting(StepResult::getStatus) - .containsExactlyInAnyOrder( - Status.PASSED, - Status.PASSED, - Status.PASSED, - Status.PASSED - ) - ), - dynamicTest( - "Second level step 1 name", () -> assertThat(testResult.get(0).getSteps().get(0).getSteps().get(0).getName()) - .contains("io.qameta.allure.awaitility.GlobalSettingsPositiveTest") - .contains("expected <3> but was <0>") - .contains("elapsed time") - .contains("remaining time") - .contains("last poll interval was") - ), - dynamicTest( - "Second level step 2 name", () -> assertThat(testResult.get(0).getSteps().get(0).getSteps().get(1).getName()) - .contains("io.qameta.allure.awaitility.GlobalSettingsPositiveTest") - .contains("expected <3> but was <1>") - .contains("elapsed time") - .contains("remaining time") - .contains("last poll interval was") - ), - dynamicTest( - "Second level step 3 name", () -> assertThat(testResult.get(0).getSteps().get(0).getSteps().get(2).getName()) - .contains("io.qameta.allure.awaitility.GlobalSettingsPositiveTest") - .contains("expected <3> but was <2>") - .contains("elapsed time") - .contains("remaining time") - .contains("last poll interval was") - ), - dynamicTest( - "Second level step 4 name", () -> assertThat(testResult.get(0).getSteps().get(0).getSteps().get(3).getName()) - .contains("io.qameta.allure.awaitility.GlobalSettingsPositiveTest") - .contains("java.util.concurrent.atomic.AtomicInteger:") - .contains("reached its end value of <3> after") - .contains("remaining time") - .contains("last poll interval was") - ) - ); + .untilAsserted(() -> assertThat(atomicInteger.getAndIncrement()).isEqualTo(3)); + }).getTestResults(); + + return testResult.get(0).getSteps(); + } + + private StepResult awaitWithoutAliasTopLevelStep() { + return runAwaitWithoutAliasTopLevelSteps().get(0); + } + + private StepResult awaitWithAliasTopLevelStep() { + final List testResult = runWithinTestContext(() -> { + final AtomicInteger atomicInteger = new AtomicInteger(0); + await("Counter should be at least 3").with() + .atMost(Duration.of(1000, ChronoUnit.MILLIS)) + .pollInterval(Duration.of(50, ChronoUnit.MILLIS)) + .untilAsserted(() -> assertThat(atomicInteger.getAndIncrement()).isEqualTo(3)); + }).getTestResults(); + + return testResult.get(0).getSteps().get(0); + } + + private List awaitWithoutAliasPollSteps() { + return awaitWithoutAliasTopLevelStep().getSteps().stream() + .filter(GlobalSettingsPositiveTest::isAwaitilityEvaluationStep) + .toList(); + } + + private static boolean isAwaitilityEvaluationStep(final StepResult step) { + return AWAITILITY_EVALUATION_DESCRIPTION.equals(step.getDescription()); + } + + private StepResult firstFailedPollStep() { + return awaitWithoutAliasPollSteps().get(0); + } + + private StepResult secondFailedPollStep() { + return awaitWithoutAliasPollSteps().get(1); + } + + private StepResult thirdFailedPollStep() { + return awaitWithoutAliasPollSteps().get(2); + } + + private StepResult successfulPollStep() { + return awaitWithoutAliasPollSteps().get(3); } } diff --git a/allure-awaitility/src/test/java/io/qameta/allure/awaitility/MultipleConditionsTest.java b/allure-awaitility/src/test/java/io/qameta/allure/awaitility/MultipleConditionsTest.java index 6b2fff5f9..dbf24053e 100644 --- a/allure-awaitility/src/test/java/io/qameta/allure/awaitility/MultipleConditionsTest.java +++ b/allure-awaitility/src/test/java/io/qameta/allure/awaitility/MultipleConditionsTest.java @@ -15,18 +15,23 @@ */ package io.qameta.allure.awaitility; +import io.qameta.allure.Description; import io.qameta.allure.model.Status; +import io.qameta.allure.model.StepResult; import io.qameta.allure.model.TestResult; +import io.qameta.allure.test.IsolatedLifecycle; import org.awaitility.Awaitility; -import org.junit.jupiter.api.*; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.util.List; -import java.util.stream.Stream; import static io.qameta.allure.test.RunUtils.runWithinTestContext; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +@IsolatedLifecycle public class MultipleConditionsTest { @AfterEach @@ -40,8 +45,33 @@ void setup() { Awaitility.setDefaultConditionEvaluationListener(new AllureAwaitilityListener()); } - @TestFactory - Stream bothAwaitilityStepsShouldAppearTest() { + /** + * Verifies that a test containing two Awaitility conditions reports a separate top-level step for each wait. + */ + @Description + @Test + void shouldRecordTopLevelStepForEachAwaitilityCondition() { + final List steps = runMultipleAwaitilityTopLevelSteps(); + + assertThat(steps) + .describedAs("Allure TestResult contains exactly 2 top level step for 2 awaitility condition") + .hasSize(2); + } + + /** + * Verifies that every top-level step created for multiple successful Awaitility conditions is marked passed. + */ + @Description + @Test + void shouldMarkEveryAwaitilityConditionStepPassed() { + final List steps = runMultipleAwaitilityTopLevelSteps(); + + assertThat(steps) + .describedAs("Allure TestResult contains all top level step for all awaitility with PASSED condition") + .allMatch(step -> Status.PASSED.equals(step.getStatus())); + } + + private List runMultipleAwaitilityTopLevelSteps() { final List testResult = runWithinTestContext(() -> { await().with() .alias("First waiting") @@ -49,22 +79,10 @@ Stream bothAwaitilityStepsShouldAppearTest() { await().with() .alias("Second waiting") .until(() -> true); - }, - AllureAwaitilityListener::setLifecycle + } ).getTestResults(); - return Stream.of( - DynamicTest.dynamicTest( - "Exactly 2 top level step for 2 awaitility condition", () -> assertThat(testResult.get(0).getSteps()) - .describedAs("Allure TestResult contains exactly 2 top level step for 2 awaitility condition") - .hasSize(2) - ), - DynamicTest.dynamicTest( - "All top level step for all awaitility condition has PASSED", () -> assertThat(testResult.get(0).getSteps()) - .describedAs("Allure TestResult contains all top level step for all awaitility with PASSED condition") - .allMatch(step -> Status.PASSED.equals(step.getStatus())) - ) - ); + return testResult.get(0).getSteps(); } } diff --git a/allure-awaitility/src/test/resources/allure.properties b/allure-awaitility/src/test/resources/allure.properties index 0486d8a71..94bba0eee 100644 --- a/allure-awaitility/src/test/resources/allure.properties +++ b/allure-awaitility/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-awaitility diff --git a/allure-bom/README.md b/allure-bom/README.md new file mode 100644 index 000000000..59f71f014 --- /dev/null +++ b/allure-bom/README.md @@ -0,0 +1,52 @@ +# allure-bom + +Bill of materials for Allure Java artifacts. + +Use this module to keep all Allure Java dependencies on the same release line. It is the recommended way to declare Allure modules in Gradle and Maven projects. + +## Supported Versions + +- Allure Java 3.x requires Java 17 or newer for runtime modules. +- The BOM aligns all artifacts published from the same Allure Java release. + +## Installation + +Gradle: + +```kotlin +dependencies { + testImplementation(platform("io.qameta.allure:allure-bom:")) + testImplementation("io.qameta.allure:allure-jupiter") + testImplementation("io.qameta.allure:allure-rest-assured") +} +``` + +Maven: + +```xml + + + + io.qameta.allure + allure-bom + ${allure.version} + pom + import + + + +``` + +After importing the BOM, omit versions from individual Allure Java dependencies. + +## What It Aligns + +- Test framework adapters such as `allure-jupiter`, `allure-testng`, and `allure-cucumber7-jvm`. +- Runtime and support APIs such as `allure-java-commons` and `allure-model`. +- HTTP, browser, assertion, and utility integrations. + +Use one Allure version for all modules in a test suite. Mixing versions can produce missing metadata, duplicate lifecycle listeners, or unreadable result files. + +## What To Expect + +The BOM does not write Allure results by itself. It only controls dependency versions. Add at least one adapter, such as `allure-jupiter`, `allure-testng`, or `allure-cucumber7-jvm`, to produce report data. diff --git a/allure-bom/build.gradle.kts b/allure-bom/build.gradle.kts index 0d83d95e9..13cebfa89 100644 --- a/allure-bom/build.gradle.kts +++ b/allure-bom/build.gradle.kts @@ -4,12 +4,19 @@ plugins { description = "Allure Java (Bill of Materials)" +val scalaTestBinaryVersions = listOf("2.12", "2.13", "3") + 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}") + rootProject.subprojects.filter { it.plugins.hasPlugin("maven-publish") }.sorted().forEach { + if (it.name == "allure-scalatest") { + scalaTestBinaryVersions.forEach { scalaBinaryVersion -> + api("${it.group}:${it.name}_$scalaBinaryVersion:${it.version}") + } + } else { + api("${it.group}:${it.name}:${it.version}") + } + } } } diff --git a/allure-citrus/README.md b/allure-citrus/README.md new file mode 100644 index 000000000..5764240f6 --- /dev/null +++ b/allure-citrus/README.md @@ -0,0 +1,42 @@ +# allure-citrus + +Citrus listener integration for Allure Java. + +Use this module when your integration tests run on Citrus and you want Citrus suites, test cases, actions, parameters, and failures to appear in Allure Report. + +## Supported Versions + +- Allure Java 3.x requires Java 17 or newer. +- This module targets the classic `com.consol.citrus` API. +- The current build validates against Citrus 3.4.1. + +## Installation + +Gradle: + +```kotlin +dependencies { + testImplementation(platform("io.qameta.allure:allure-bom:")) + testImplementation("io.qameta.allure:allure-citrus") +} +``` + +Maven, with `allure-bom` imported in dependency management: + +```xml + + io.qameta.allure + allure-citrus + test + +``` + +## Setup + +Register `io.qameta.allure.citrus.AllureCitrus` with Citrus as a test, test suite, and test action listener. The listener translates Citrus suite, test case, and action events into Allure tests and steps. + +## Report Output + +- Citrus test cases and suite lifecycle. +- Test actions as Allure steps. +- Labels, links, parameters, status, and status details. diff --git a/allure-citrus/build.gradle.kts b/allure-citrus/build.gradle.kts index dbad67a9c..7184fd620 100644 --- a/allure-citrus/build.gradle.kts +++ b/allure-citrus/build.gradle.kts @@ -1,6 +1,6 @@ description = "Allure Citrus Integration" -val citrusVersion = "2.8.0" +val citrusVersion = "3.4.1" dependencies { api(project(":allure-java-commons")) @@ -16,6 +16,7 @@ dependencies { testImplementation(project(":allure-java-commons-test")) testImplementation(project(":allure-junit-platform")) testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") } tasks.jar { diff --git a/allure-citrus/src/main/java/io/qameta/allure/citrus/AllureCitrus.java b/allure-citrus/src/main/java/io/qameta/allure/citrus/AllureCitrus.java index cc5e669d6..a9ed3a86c 100644 --- a/allure-citrus/src/main/java/io/qameta/allure/citrus/AllureCitrus.java +++ b/allure-citrus/src/main/java/io/qameta/allure/citrus/AllureCitrus.java @@ -21,6 +21,7 @@ import com.consol.citrus.report.TestListener; import com.consol.citrus.report.TestSuiteListener; import io.qameta.allure.Allure; +import io.qameta.allure.AllureExternalKey; import io.qameta.allure.AllureLifecycle; import io.qameta.allure.Description; import io.qameta.allure.Epic; @@ -29,7 +30,6 @@ import io.qameta.allure.model.Label; import io.qameta.allure.model.Link; import io.qameta.allure.model.Parameter; -import io.qameta.allure.model.Stage; import io.qameta.allure.model.Status; import io.qameta.allure.model.StatusDetails; import io.qameta.allure.model.StepResult; @@ -40,13 +40,13 @@ import java.lang.annotation.Repeatable; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -58,9 +58,11 @@ import static io.qameta.allure.util.ResultsUtils.createLanguageLabel; import static io.qameta.allure.util.ResultsUtils.createParameter; import static io.qameta.allure.util.ResultsUtils.createSuiteLabel; +import static io.qameta.allure.util.ResultsUtils.createTestClassLabel; import static io.qameta.allure.util.ResultsUtils.createThreadLabel; import static io.qameta.allure.util.ResultsUtils.createTitlePath; import static io.qameta.allure.util.ResultsUtils.getProvidedLabels; +import static io.qameta.allure.util.ResultsUtils.md5; /** * Reports Citrus test execution to Allure. @@ -69,7 +71,7 @@ */ public class AllureCitrus implements TestListener, TestSuiteListener, TestActionListener { - private final Map testUuids = new ConcurrentHashMap<>(); + private final Map testKeys = new ConcurrentHashMap<>(); private final ReadWriteLock lock = new ReentrantReadWriteLock(); @@ -154,7 +156,7 @@ public void onFinishFailure(final Throwable cause) { */ @Override public void onTestStart(final TestCase test) { - startTestCase(test); + startTest(test); } /** @@ -170,7 +172,7 @@ public void onTestFinish(final TestCase test) { */ @Override public void onTestSuccess(final TestCase test) { - stopTestCase(test, Status.PASSED, null); + stopTest(test, Status.PASSED, null); } /** @@ -180,7 +182,7 @@ public void onTestSuccess(final TestCase test) { public void onTestFailure(final TestCase test, final Throwable cause) { final Status status = ResultsUtils.getStatus(cause).orElse(Status.BROKEN); final StatusDetails details = ResultsUtils.getStatusDetails(cause).orElse(null); - stopTestCase(test, status, details); + stopTest(test, status, details); } /** @@ -188,7 +190,10 @@ public void onTestFailure(final TestCase test, final Throwable cause) { */ @Override public void onTestSkipped(final TestCase test) { - //do nothing + if (!isTestStarted(test)) { + startTest(test); + } + stopTest(test, Status.SKIPPED, null); } /** @@ -196,9 +201,7 @@ public void onTestSkipped(final TestCase test) { */ @Override public void onTestActionStart(final TestCase testCase, final TestAction testAction) { - final String parentUuid = getUuid(testCase); - final String uuid = UUID.randomUUID().toString(); - getLifecycle().startStep(parentUuid, uuid, new StepResult().setName(testAction.getName())); + getLifecycle().startStep(new StepResult().setName(testAction.getName())); } /** @@ -217,25 +220,33 @@ public void onTestActionSkipped(final TestCase testCase, final TestAction testAc //do nothing } - private void startTestCase(final TestCase testCase) { - final String uuid = createUuid(testCase); + private void startTest(final TestCase testCase) { + final AllureExternalKey testKey = createTestKey(testCase); final Optional> testClass = Optional.ofNullable(testCase.getTestClass()); + final String fullName = testClass + .map(Class::getName) + .map(className -> className + "." + testCase.getName()) + .orElseGet(testCase::getName); final TestResult result = new TestResult() - .setUuid(uuid) .setName(testCase.getName()) + .setFullName(fullName) + .setTestCaseId(md5(fullName)) .setTitlePath( testClass .map(ResultsUtils::createTitlePathFromJavaClass) .orElseGet(() -> createTitlePath(testCase.getName())) - ) - .setStage(Stage.RUNNING); + ); result.getLabels().addAll(getProvidedLabels()); testClass.map(this::getLabels).ifPresent(result.getLabels()::addAll); testClass.map(this::getLinks).ifPresent(result.getLinks()::addAll); + // the test is represented by a class only when the test case is class-backed (java dsl); + // the test method stays unknown — the test case name may be customized by the user + testClass.ifPresent(aClass -> result.getLabels().add(createTestClassLabel(aClass.getName()))); + result.getLabels().addAll( Arrays.asList( createHostLabel(), @@ -245,9 +256,10 @@ private void startTestCase(final TestCase testCase) { ) ); + final List