diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..bb09719d9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,85 @@ +name: 🐞 Bug Report +description: File a Bug report in Java Integration +title: "🐞: " +labels: [ "type:bug", "triage" ] +assignees: [] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Also tell us, what did you expect to happen? + placeholder: Tell us what you see! + value: "A bug happened!" + validations: + required: true + - type: dropdown + id: integration + attributes: + label: What Allure Integration are you using? + multiple: true + description: Please select the Allure integration you + options: + - allure-assertj + - allure-attachments + - allure-awaitility + - 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-java-commons + - allure-jax-rs + - allure-jbehave + - allure-jbehave5 + - allure-jsonunit + - allure-junit-platform + - allure-junit4 + - allure-junit5 + - allure-karate + - allure-okhttp + - allure-okhttp3 + - allure-reader + - allure-rest-assured + - allure-scalatest + - allure-selenide + - allure-servlet-api + - allure-spock + - allure-spock2 + - allure-spring-web + - allure-test-filter + - allure-testng + validations: + required: true + - type: input + id: integration_version + attributes: + label: What version of Allure Integration you are using? + placeholder: 2.22.3 + validations: + required: true + - type: input + id: allure_report_version + attributes: + label: What version of Allure Report you are using? + placeholder: 2.22.3 + validations: + required: true + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/allure-framework/allure-java/blob/main/CODE_OF_CONDUCT.md) + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..fb81bedae --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: ⭐️ Allure Report | main repository | give us a ⭐️ + url: https://github.com/allure-framework/allure2 + about: For Allure Report related issues. + - name: 💬 Allure Report Community - for bugs, issues, dedicated support and more! + url: https://github.com/orgs/allure-framework/discussions + about: Please ask and answer questions here. + - name: 💚 Allure TestOps Support + url: https://help.qameta.io/support/home + about: Please report Allure TestOps issues here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0dec83312..8178c9b61 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,7 +3,7 @@ Thank you so much for sending us a pull request! Make sure you have a clear name for your pull request. The name should start with a capital letter and no dot is required in the end of the sentence. -To link the request with isses use the following notation: (fixes #123, fixes #321\) +To link the request with issues use the following notation: (fixes #123, fixes #321\) An example of good pull request names: * Add Cucumber integration (fixes #123\) diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..240d09840 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + labels: + - "type:dependencies" + + - package-ecosystem: "gradle" + directory: "/" + schedule: + interval: "daily" + labels: + - "type:dependencies" diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 000000000..a419c97f9 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,84 @@ +"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-junit5/**" + - "allure-junit5-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/release.yml b/.github/release.yml new file mode 100644 index 000000000..7c30a5979 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,25 @@ +# release.yml + +changelog: + categories: + - title: '🚀 New Features' + labels: + - 'type:new feature' + - title: '🔬 Improvements' + labels: + - 'type:improvement' + - title: '🐞 Bug Fixes' + labels: + - 'type:bug' + - title: '⬆️ Dependency Updates' + labels: + - 'type:dependencies' + - title: '📖 Documentation improvements' + labels: + - 'type:documentation' + - title: '⛔️ Security' + labels: + - 'type:security' + - title: '👻 Internal changes' + labels: + - 'type:internal' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..8f537bc9c --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,34 @@ +name: Build + +permissions: + contents: read + +on: + workflow_dispatch: + pull_request: + branches: + - '*' + push: + branches: + - 'main' + - 'hotfix-*' + +jobs: + build: + name: "Build" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: "Set up JDK" + uses: actions/setup-java@v5 + with: + distribution: 'zulu' + java-version: 21 + + - name: "Build with Gradle" + run: ./gradlew build -x test --scan + + - name: "Run tests" + if: always() + run: ./gradlew --no-build-cache cleanTest test diff --git a/.github/workflows/dependency-submission.yml b/.github/workflows/dependency-submission.yml new file mode 100644 index 000000000..748dfe670 --- /dev/null +++ b/.github/workflows/dependency-submission.yml @@ -0,0 +1,22 @@ +name: Dependency Submission + +on: + push: + branches: + - main + +permissions: + contents: write + +jobs: + dependency-submission: + name: Dependency Submission + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v6 + - name: Generate and submit dependency graph + uses: gradle/actions/dependency-submission@v5 + env: + DEPENDENCY_GRAPH_EXCLUDE_PROJECTS: ':allure-java-commons-test' + DEPENDENCY_GRAPH_INCLUDE_CONFIGURATIONS: 'runtimeClasspath' diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 000000000..f49976b59 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,17 @@ +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 new file mode 100644 index 000000000..7315a905a --- /dev/null +++ b/.github/workflows/labels-verify.yml @@ -0,0 +1,27 @@ +name: "Verify type labels" + +on: + pull_request_target: + types: [opened, labeled, unlabeled, synchronize] + +permissions: + contents: none + +jobs: + triage: + 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 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..2295deb90 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,41 @@ +name: Publish + +on: + release: + types: [ published ] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: "Set up JDK" + uses: actions/setup-java@v5 + with: + distribution: 'zulu' + java-version: '21' + + - name: Set up GPG + run: echo -n "${GPG_PRIVATE_KEY}" | base64 --decode > ${GITHUB_WORKSPACE}/${GPG_KEY_ID}.gpg + env: + GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + + - name: "Gradle Build" + run: ./gradlew build -Pversion=${GITHUB_REF:10} + + - name: "Gradle Publish" + run: | + ./gradlew publishToSonatype closeSonatypeStagingRepository -Pversion=${GITHUB_REF:10} \ + -Psigning.keyId=${GPG_KEY_ID} \ + -Psigning.password=${GPG_PASSPHRASE} \ + -Psigning.secretKeyRingFile=${GITHUB_WORKSPACE}/${GPG_KEY_ID}.gpg + env: + ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.OSSRH_USERNAME }} + ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.OSSRH_PASSWORD }} + GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..7d5ace556 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,62 @@ +name: Release +run-name: Release ${{ inputs.releaseVersion }} (next ${{ inputs.nextVersion }}) by ${{ github.actor }} + +on: + workflow_dispatch: + inputs: + releaseVersion: + description: "The release version in .. format" + required: true + nextVersion: + description: "The next version in . format WITHOUT SNAPSHOT SUFFIX" + required: true + +permissions: + contents: read + +jobs: + triage: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: "Check release version" + run: | + expr "${{ github.event.inputs.releaseVersion }}" : '[[:digit:]][[:digit:]]*\.[[:digit:]][[:digit:]]*\.[[:digit:]][[:digit:]]*$' + - name: "Check next version" + run: | + expr "${{ github.event.inputs.nextVersion }}" : '[[:digit:]][[:digit:]]*\.[[:digit:]][[:digit:]]*$' + - uses: actions/checkout@v6 + with: + token: ${{ secrets.QAMETA_CI }} + + - name: "Configure CI Git User" + run: | + git config --global user.name qameta-ci + git config --global user.email qameta-ci@qameta.io + - name: "Set release version" + run: | + sed -i -e '/version=/s/.*/version=${{ github.event.inputs.releaseVersion }}/g' gradle.properties + cat gradle.properties + - name: "Commit release version and create tag" + run: | + git commit -am "release ${{ github.event.inputs.releaseVersion }}" + git tag ${{ github.event.inputs.releaseVersion }} + git push origin ${{ github.event.inputs.releaseVersion }} + - name: "Set next development version" + run: | + sed -i -e '/version=/s/.*/version=${{ github.event.inputs.nextVersion }}-SNAPSHOT/g' gradle.properties + cat gradle.properties + - name: "Commit next development version and push it" + run: | + git commit -am "set next development version ${{ github.event.inputs.nextVersion }}" + git push origin ${{ github.ref }} + - name: "Publish Github Release" + uses: octokit/request-action@v2.x + with: + route: POST /repos/${{ github.repository }}/releases + tag_name: ${{ github.event.inputs.releaseVersion }} + generate_release_notes: true + target_commitish: ${{ github.ref }} + env: + GITHUB_TOKEN: ${{ secrets.QAMETA_CI }} diff --git a/.gitignore b/.gitignore index b40a243dd..b85107a90 100644 --- a/.gitignore +++ b/.gitignore @@ -4,9 +4,12 @@ schema build /*/allure-results/ out +.gradletasknamecache #IDEA Files -.idea +.idea/* +!.idea/vcs.xml +!.idea/icon.png *.iml *.ipr @@ -20,4 +23,4 @@ out .DS_Store #Netbeans files -/.nb-gradle/ \ No newline at end of file +/.nb-gradle/ diff --git a/.idea/icon.png b/.idea/icon.png new file mode 100644 index 000000000..252994657 Binary files /dev/null and b/.idea/icon.png differ diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..aeaa9e459 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/AUTHORS b/AUTHORS index 0edbd3fb9..8263d9471 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,4 +1,4 @@ The following authors have created the source code of "Allure Java" -published and distributed by Qameta Software OÜ as the owner: +published and distributed by Qameta Software Inc as the owner: * Dmitry Baev diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index 64bac5a9e..000000000 --- a/Jenkinsfile +++ /dev/null @@ -1,40 +0,0 @@ -pipeline { - agent { label 'java' } - parameters { - booleanParam(name: 'RELEASE', defaultValue: false, description: 'Perform release?') - string(name: 'RELEASE_VERSION', defaultValue: '', description: 'Release version') - string(name: 'NEXT_VERSION', defaultValue: '', description: 'Next version (without SNAPSHOT)') - } - stages { - stage('Build') { - steps { - sh './gradlew build' - } - } - stage('Release') { - when { expression { return params.RELEASE } } - steps { - withCredentials([usernamePassword(credentialsId: 'qameta-ci_bintray', - usernameVariable: 'BINTRAY_USER', passwordVariable: 'BINTRAY_API_KEY')]) { - sshagent(['qameta-ci_ssh']) { - sh 'git checkout master && git pull origin master' - sh "./gradlew release -Prelease.useAutomaticVersion=true " + - "-Prelease.releaseVersion=${RELEASE_VERSION} " + - "-Prelease.newVersion=${NEXT_VERSION}-SNAPSHOT" - } - } - } - } - } - post { - always { - allure results: [[path: '**/build/allure-results']] - deleteDir() - } - - failure { - slackSend message: "${env.JOB_NAME} - #${env.BUILD_NUMBER} failed (<${env.BUILD_URL}|Open>)", - color: 'danger', teamDomain: 'qameta', channel: 'allure', tokenCredentialId: 'allure-channel' - } - } -} diff --git a/LICENSE b/LICENSE index a5ff24fc6..aa0c10ae1 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2019 Qameta Software OÜ + Copyright 2016-2024 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. @@ -198,4 +198,4 @@ 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. \ No newline at end of file + limitations under the License. diff --git a/README.md b/README.md index e96e77409..e452787b3 100644 --- a/README.md +++ b/README.md @@ -1,171 +1,242 @@ -[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" - -[bintray]: https://bintray.com/qameta/maven/allure-java "Bintray" -[bintray-badge]: https://img.shields.io/bintray/v/qameta/maven/allure-java.svg?style=flat - -[CONTRIBUTING.md]: .github/CONTRIBUTING.md -[docs]: https://docs.qameta.io/allure/2.0/ - -# Allure Java Integrations [![bintray-badge][]][bintray] - -The repository contains new versions of adaptors for JVM-based test frameworks. - -All the artifacts are deployed to `https://dl.bintray.com/qameta/maven`. - -## TestNG - -The new TestNG adaptors is pretty much ready. To use the adaptor you should add the following dependency: - -```xml - - io.qameta.allure - allure-testng - $LATEST_VERSION - -``` - -also you need to configure AspectJ weaver to support steps. - -## JUnit 4 - -The first draft of a new JUnit 4 adaptor is ready. To use the adaptor you should add the following dependency: - -```xml - - io.qameta.allure - allure-junit4 - $LATEST_VERSION - -``` - -## JUnit 5 - -To use JUnit 5 simply add the following dependency to your project: - -```xml - - io.qameta.allure - allure-junit5 - $LATEST_VERSION - -``` - -## 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 -``` - - -## 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")) -``` - -## OkHttp - -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")) - -``` - -## 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()); -``` - -## 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 - -``` +[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 5 + +- 🚀 Documentation — https://allurereport.org/docs/junit5/ +- 📚 Example project — https://github.com/allure-examples?q=topic%3Ajunit5 +- ✅ Generate a project in 10 seconds via Allure Start - https://allurereport.org/start/ + +## 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 +``` + + +## 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")) +``` + +## OkHttp + +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()); +``` + diff --git a/allure-assertj/build.gradle.kts b/allure-assertj/build.gradle.kts index e8e064ed9..e38cca0ea 100644 --- a/allure-assertj/build.gradle.kts +++ b/allure-assertj/build.gradle.kts @@ -1,9 +1,6 @@ description = "Allure AssertJ Integration" -val agent: Configuration by configurations.creating - dependencies { - agent("org.aspectj:aspectjweaver") api(project(":allure-java-commons")) compileOnly("org.aspectj:aspectjrt") compileOnly("org.assertj:assertj-core") @@ -25,7 +22,4 @@ tasks.jar { tasks.test { useJUnitPlatform() - doFirst { - jvmArgs("-javaagent:${agent.singleFile}") - } } 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 e54ca815e..128791172 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 @@ -1,5 +1,5 @@ /* - * Copyright 2019 Qameta Software OÜ + * Copyright 2016-2024 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. @@ -55,7 +55,7 @@ protected AllureLifecycle initialValue() { } }; - @Pointcut("execution(public org.assertj.core.api.AbstractAssert.new(..))") + @Pointcut("execution(!private org.assertj.core.api.AbstractAssert.new(..))") public void anyAssertCreation() { //pointcut body, should be empty } 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 7a48210c8..57ddef240 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 @@ -1,5 +1,5 @@ /* - * Copyright 2019 Qameta Software OÜ + * Copyright 2016-2024 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. @@ -23,6 +23,7 @@ import org.junit.jupiter.api.Test; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import static io.qameta.allure.test.RunUtils.runWithinTestContext; import static org.assertj.core.api.Assertions.assertThat; @@ -82,11 +83,28 @@ void shouldHandleByteArrayObject() { .extracting(StepResult::getName) .containsExactly( "assertThat ''", - "as 'Byte array object []'", + "describedAs 'Byte array object'", "isEqualTo ''" ); } + @AllureFeatures.Steps + @Test + void shouldHandleCollections() { + final AllureResults results = runWithinTestContext(() -> { + assertThat(Arrays.asList("a", "b")) + .containsExactly("a", "b"); + }, AllureAspectJ::setLifecycle); + + assertThat(results.getTestResults()) + .flatExtracting(TestResult::getSteps) + .extracting(StepResult::getName) + .containsExactly( + "assertThatList '[a, b]'", + "containsExactly '[a, b]'" + ); + } + @AllureFeatures.Steps @Test void softAssertions() { diff --git a/allure-attachments/build.gradle.kts b/allure-attachments/build.gradle.kts index bfbe95e7c..c5010a3c0 100644 --- a/allure-attachments/build.gradle.kts +++ b/allure-attachments/build.gradle.kts @@ -1,9 +1,6 @@ description = "Allure Attachments" -val agent: Configuration by configurations.creating - dependencies { - agent("org.aspectj:aspectjweaver") api(project(":allure-java-commons")) implementation("org.freemarker:freemarker") testImplementation("org.apache.commons:commons-lang3") @@ -26,7 +23,4 @@ tasks.jar { tasks.test { useJUnitPlatform() - doFirst { - jvmArgs("-javaagent:${agent.singleFile}") - } } 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 index c8fe7c111..50af67ea4 100644 --- a/allure-attachments/src/main/java/io/qameta/allure/attachment/AttachmentContent.java +++ b/allure-attachments/src/main/java/io/qameta/allure/attachment/AttachmentContent.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Qameta Software OÜ + * Copyright 2016-2024 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. diff --git a/allure-attachments/src/main/java/io/qameta/allure/attachment/AttachmentData.java b/allure-attachments/src/main/java/io/qameta/allure/attachment/AttachmentData.java index 86b318a05..5c6d09f34 100644 --- a/allure-attachments/src/main/java/io/qameta/allure/attachment/AttachmentData.java +++ b/allure-attachments/src/main/java/io/qameta/allure/attachment/AttachmentData.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Qameta Software OÜ + * Copyright 2016-2024 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. @@ -20,6 +20,7 @@ * * @author charlie (Dmitry Baev). */ +@FunctionalInterface public interface AttachmentData { String getName(); diff --git a/allure-attachments/src/main/java/io/qameta/allure/attachment/AttachmentProcessor.java b/allure-attachments/src/main/java/io/qameta/allure/attachment/AttachmentProcessor.java index cfc93c716..99129f89a 100644 --- a/allure-attachments/src/main/java/io/qameta/allure/attachment/AttachmentProcessor.java +++ b/allure-attachments/src/main/java/io/qameta/allure/attachment/AttachmentProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Qameta Software OÜ + * Copyright 2016-2024 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. @@ -19,6 +19,7 @@ * @param the type of attachment data. * @author charlie (Dmitry Baev). */ +@FunctionalInterface public interface AttachmentProcessor { void addAttachment(T attachmentData, AttachmentRenderer renderer); 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 index 5d702bffa..daaf9cc91 100644 --- a/allure-attachments/src/main/java/io/qameta/allure/attachment/AttachmentRenderException.java +++ b/allure-attachments/src/main/java/io/qameta/allure/attachment/AttachmentRenderException.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Qameta Software OÜ + * Copyright 2016-2024 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. diff --git a/allure-attachments/src/main/java/io/qameta/allure/attachment/AttachmentRenderer.java b/allure-attachments/src/main/java/io/qameta/allure/attachment/AttachmentRenderer.java index 1d239331e..2b6f5b1a5 100644 --- a/allure-attachments/src/main/java/io/qameta/allure/attachment/AttachmentRenderer.java +++ b/allure-attachments/src/main/java/io/qameta/allure/attachment/AttachmentRenderer.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Qameta Software OÜ + * Copyright 2016-2024 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. @@ -20,6 +20,7 @@ * @author charlie (Dmitry Baev). */ @SuppressWarnings("PMD.AvoidUncheckedExceptionsInSignatures") +@FunctionalInterface public interface AttachmentRenderer { AttachmentContent render(T attachmentData) throws AttachmentRenderException; 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 index ab26abbb6..ccdc978a8 100644 --- a/allure-attachments/src/main/java/io/qameta/allure/attachment/DefaultAttachmentContent.java +++ b/allure-attachments/src/main/java/io/qameta/allure/attachment/DefaultAttachmentContent.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Qameta Software OÜ + * Copyright 2016-2024 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. 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 index dc57e9a01..652c8ca5f 100644 --- a/allure-attachments/src/main/java/io/qameta/allure/attachment/DefaultAttachmentProcessor.java +++ b/allure-attachments/src/main/java/io/qameta/allure/attachment/DefaultAttachmentProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Qameta Software OÜ + * Copyright 2016-2024 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. 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 index 9179f3ed7..50ac53924 100644 --- a/allure-attachments/src/main/java/io/qameta/allure/attachment/FreemarkerAttachmentRenderer.java +++ b/allure-attachments/src/main/java/io/qameta/allure/attachment/FreemarkerAttachmentRenderer.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Qameta Software OÜ + * Copyright 2016-2024 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. @@ -17,6 +17,9 @@ 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; @@ -27,6 +30,8 @@ */ public class FreemarkerAttachmentRenderer implements AttachmentRenderer { + private static final Logger LOGGER = LoggerFactory.getLogger(FreemarkerAttachmentRenderer.class); + private final Configuration configuration; private final String templateName; @@ -36,6 +41,7 @@ public FreemarkerAttachmentRenderer(final String 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"); } @@ -46,6 +52,7 @@ public DefaultAttachmentContent render(final AttachmentData data) { 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 index 831b2a5da..a51052cf7 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2019 Qameta Software OÜ + * Copyright 2016-2024 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. @@ -16,7 +16,9 @@ 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; @@ -40,9 +42,18 @@ public class HttpRequestAttachment implements AttachmentData { private final Map cookies; + private final Map formParams; + 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()); + } + + @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; @@ -50,6 +61,7 @@ public HttpRequestAttachment(final String name, final String url, final String m this.curl = curl; this.headers = headers; this.cookies = cookies; + this.formParams = formParams; } public String getUrl() { @@ -72,6 +84,10 @@ public Map getCookies() { return cookies; } + public Map getFormParams() { + return formParams; + } + public String getCurl() { return curl; } @@ -81,10 +97,21 @@ public String getName() { return name; } + @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. */ - @SuppressWarnings("PMD.AvoidFieldNameMatchingMethodName") public static final class Builder { private final String name; @@ -99,6 +126,8 @@ public static final class Builder { 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"); @@ -148,6 +177,12 @@ public Builder setBody(final String body) { return this; } + 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 @@ -203,7 +238,7 @@ public Builder withBody(final String body) { } public HttpRequestAttachment build() { - return new HttpRequestAttachment(name, url, method, body, getCurl(), headers, cookies); + return new HttpRequestAttachment(name, url, method, body, getCurl(), headers, cookies, formParams); } private String getCurl() { @@ -214,6 +249,7 @@ private String getCurl() { 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('\''); @@ -236,5 +272,13 @@ private static void appendCookie(final StringBuilder builder, final String key, .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 index d73b21701..d606af57e 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2019 Qameta Software OÜ + * Copyright 2016-2024 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. @@ -16,6 +16,7 @@ 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; @@ -74,10 +75,21 @@ public Map getCookies() { return cookies; } + @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. */ - @SuppressWarnings("PMD.AvoidFieldNameMatchingMethodName") public static final class Builder { private final String name; @@ -108,7 +120,6 @@ public Builder setUrl(final String url) { } public Builder setResponseCode(final int responseCode) { - Objects.requireNonNull(responseCode, "Response code must not be null value"); this.responseCode = responseCode; return this; } @@ -147,6 +158,7 @@ public Builder setBody(final String body) { /** * Use setter method instead. + * * @deprecated scheduled for removal in 3.0 release */ @Deprecated @@ -156,6 +168,7 @@ public Builder withUrl(final String url) { /** * Use setter method instead. + * * @deprecated scheduled for removal in 3.0 release */ @Deprecated @@ -165,6 +178,7 @@ public Builder withResponseCode(final int responseCode) { /** * Use setter method instead. + * * @deprecated scheduled for removal in 3.0 release */ @Deprecated @@ -174,6 +188,7 @@ public Builder withHeader(final String name, final String value) { /** * Use setter method instead. + * * @deprecated scheduled for removal in 3.0 release */ @Deprecated @@ -183,6 +198,7 @@ public Builder withHeaders(final Map headers) { /** * Use setter method instead. + * * @deprecated scheduled for removal in 3.0 release */ @Deprecated @@ -192,6 +208,7 @@ public Builder withCookie(final String name, final String value) { /** * Use setter method instead. + * * @deprecated scheduled for removal in 3.0 release */ @Deprecated @@ -201,6 +218,7 @@ public Builder withCookies(final Map cookies) { /** * Use setter method instead. + * * @deprecated scheduled for removal in 3.0 release */ @Deprecated diff --git a/allure-attachments/src/main/resources/tpl/http-request.ftl b/allure-attachments/src/main/resources/tpl/http-request.ftl index 5ef76b521..935c1c91a 100644 --- a/allure-attachments/src/main/resources/tpl/http-request.ftl +++ b/allure-attachments/src/main/resources/tpl/http-request.ftl @@ -3,36 +3,45 @@
<#if data.method??>${data.method}<#else>GET to <#if data.url??>${data.url}<#else>Unknown
<#if data.body??> -

Body

-
+

Body

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

Headers

-
- <#list data.headers as name, value> -
${name}: ${value}
- -
+

Headers

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

Cookies

-
- <#list data.cookies as name, value> -
${name}: ${value}
- -
+

Cookies

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

Curl

-
-${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 index 2bc55de9f..91fed7287 100644 --- a/allure-attachments/src/main/resources/tpl/http-response.ftl +++ b/allure-attachments/src/main/resources/tpl/http-response.ftl @@ -7,7 +7,7 @@

Body

-    ${data.body}
+    <#t>${data.body}
     
@@ -16,7 +16,7 @@

Headers

<#list data.headers as name, value> -
${name}: ${value}
+
${name}: ${value!"null"}
@@ -26,7 +26,7 @@

Cookies

<#list data.cookies as name, value> -
${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 index 1283d6116..a1d2def2e 100644 --- a/allure-attachments/src/test/java/io/qameta/allure/attachment/DefaultAttachmentProcessorTest.java +++ b/allure-attachments/src/test/java/io/qameta/allure/attachment/DefaultAttachmentProcessorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Qameta Software OÜ + * Copyright 2016-2024 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. 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 index 75ddac32d..0a773e59f 100644 --- a/allure-attachments/src/test/java/io/qameta/allure/attachment/FreemarkerAttachmentRendererTest.java +++ b/allure-attachments/src/test/java/io/qameta/allure/attachment/FreemarkerAttachmentRendererTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Qameta Software OÜ + * Copyright 2016-2024 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. @@ -16,10 +16,12 @@ 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; /** @@ -27,6 +29,13 @@ */ 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() { @@ -35,21 +44,21 @@ void shouldRenderRequestAttachment() { .render(data); assertThat(content) - .hasFieldOrPropertyWithValue("contentType", "text/html") - .hasFieldOrPropertyWithValue("fileExtension", ".html") - .hasFieldOrProperty("content"); + .hasFieldOrPropertyWithValue(CONTENT_TYPE, TEXT_HTML) + .hasFieldOrPropertyWithValue(FILE_EXTENSION, HTML) + .hasFieldOrProperty(CONTENT); } @AllureFeatures.Attachments @Test void shouldRenderResponseAttachment() { - final HttpRequestAttachment data = randomHttpRequestAttachment(); + final HttpResponseAttachment data = randomHttpResponseAttachment(); final DefaultAttachmentContent content = new FreemarkerAttachmentRenderer("http-response.ftl") .render(data); assertThat(content) - .hasFieldOrPropertyWithValue("contentType", "text/html") - .hasFieldOrPropertyWithValue("fileExtension", ".html") - .hasFieldOrProperty("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 new file mode 100644 index 000000000..fbc2e6611 --- /dev/null +++ b/allure-attachments/src/test/java/io/qameta/allure/attachment/NegativeFreemarkerAttachmentRendererTest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2016-2024 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; + +/** + * @author a-simeshin (Simeshin Artem). + */ +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 index 26eba1c6f..f5933c6db 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2019 Qameta Software OÜ + * Copyright 2016-2024 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. @@ -69,6 +69,20 @@ 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 new file mode 100644 index 000000000..813425567 --- /dev/null +++ b/allure-attachments/src/test/resources/tpl/body-npe-non-safe-attachment.ftl @@ -0,0 +1,36 @@ +<#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-cucumber3-jvm/build.gradle.kts b/allure-awaitility/build.gradle.kts similarity index 56% rename from allure-cucumber3-jvm/build.gradle.kts rename to allure-awaitility/build.gradle.kts index 34d1b0764..3c9ed6c79 100644 --- a/allure-cucumber3-jvm/build.gradle.kts +++ b/allure-awaitility/build.gradle.kts @@ -1,17 +1,16 @@ -description = "Allure CucumberJVM 3.0 Integration" +description = "Allure Awaitlity Integration" val agent: Configuration by configurations.creating -val cucumberVersion = "3.0.0" +val awaitilityVersion = "4.3.0" dependencies { agent("org.aspectj:aspectjweaver") api(project(":allure-java-commons")) - implementation("io.cucumber:cucumber-core:$cucumberVersion") - implementation("io.cucumber:cucumber-java:$cucumberVersion") - testImplementation("commons-io:commons-io") - testImplementation("io.github.glytching:junit-extensions") + compileOnly("org.awaitility:awaitility:$awaitilityVersion") + testImplementation("javax.annotation:javax.annotation-api") testImplementation("org.assertj:assertj-core") + testImplementation("org.awaitility:awaitility:$awaitilityVersion") testImplementation("org.junit.jupiter:junit-jupiter-api") testImplementation("org.slf4j:slf4j-simple") testImplementation(project(":allure-java-commons-test")) @@ -22,14 +21,11 @@ dependencies { tasks.jar { manifest { attributes(mapOf( - "Automatic-Module-Name" to "io.qameta.allure.cucumber3jvm" + "Automatic-Module-Name" to "io.qameta.allure.awaitility" )) } } tasks.test { useJUnitPlatform() - doFirst { - jvmArgs("-javaagent:${agent.singleFile}") - } } diff --git a/allure-awaitility/readme.md b/allure-awaitility/readme.md new file mode 100644 index 000000000..1fcb34691 --- /dev/null +++ b/allure-awaitility/readme.md @@ -0,0 +1,71 @@ +## 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 new file mode 100644 index 000000000..01b793cf5 --- /dev/null +++ b/allure-awaitility/src/main/java/io/qameta/allure/awaitility/AllureAwaitilityListener.java @@ -0,0 +1,257 @@ +/* + * Copyright 2016-2024 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.AllureLifecycle; +import io.qameta.allure.model.Status; +import io.qameta.allure.model.StepResult; +import org.awaitility.Awaitility; +import org.awaitility.core.ConditionEvaluationListener; +import org.awaitility.core.ConditionFactory; +import org.awaitility.core.EvaluatedCondition; +import org.awaitility.core.IgnoredException; +import org.awaitility.core.StartEvaluationEvent; +import org.awaitility.core.TimeoutEvent; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +/** + *

+ * Class implements Awaitility condition listener to log all awaiting and polls as {@link io.qameta.allure.Step}. + *

+ *

+ * Usage with single condition + *

+ * 
+ * Awaitility.await()
+ *     .conditionEvaluationListener(new AllureAwaitilityListener())
+ *     .until(() -> somethingHappen());
+ * 
+ * 
+ *

+ *

+ * Usage globally for all conditions + *

+ * 
+ * Awaitility.setDefaultConditionEvaluationListener(new AllureAwaitilityListener());
+ * 
+ * 
+ *

+ * + * @author a-simeshin (Simeshin Artem) + * @see org.awaitility.core.ConditionEvaluationListener + * @see Awaitility#setDefaultConditionEvaluationListener(ConditionEvaluationListener) + * @see ConditionFactory#conditionEvaluationListener(ConditionEvaluationListener) + * @see awaitlity wiki + */ +@SuppressWarnings("unused") +public class AllureAwaitilityListener implements ConditionEvaluationListener { + + private TimeUnit unit; + private boolean logIgnoredExceptions; + 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(); + } + }; + + public static AllureLifecycle getLifecycle() { + return LIFECYCLE.get(); + } + + /** + * 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))"; + this.onTimeoutStepTextPattern = "Condition timeout. %s"; + this.onExceptionStepTextPattern = "Exception ignored. %s"; + } + + /** + * Set default timeunit used in awaitility in project for properly printing. + * + * @param unit default timeunit in project + * @return this factory + */ + public AllureAwaitilityListener setUnit(final TimeUnit unit) { + this.unit = unit; + return this; + } + + /** + * Set logging ignored exceptions. True by default. + * + * @param logging to log or not + * @return this factory + */ + public AllureAwaitilityListener setLogIgnoredExceptions(final boolean logging) { + this.logIgnoredExceptions = logging; + return this; + } + + /** + * Method creates top-level step with short description about condition. + * + * @param startEvaluationEvent condition evaluation started + */ + @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) + ); + } + + /** + * Logging timeout evaluation result. Method should create second-level step with useful info about timeout. + * + * @param timeoutEvent poling ended with timeout + */ + @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); + } + + /** + * Logging any evaluation result. Method should create second-level step with evaluation result and useful info. + * + * @param condition evaluation result for poling iteration + */ + @Override + public void conditionEvaluated(final EvaluatedCondition condition) { + 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 String message = String.format( + condition.isSatisfied() ? onSatisfiedStepTextPattern : onAwaitStepTextPattern, + description, + elapsedTime, + unitAsString, + remainingTime, + unitAsString, + 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) + ); + getLifecycle().stopStep(lastAwaitStepUUID); + if (condition.isSatisfied()) { + awaitilityCondition.setStatus(Status.PASSED); + getLifecycle().stopStep(currentConditionStepUUID); + } + }); + } + + /** + * Logging ignored exceptions while poling conditions. Active only with + *
await().with().ignoreExceptions()
+ * or + *
Awaitility.ignoreExceptionsByDefault()
+ * + * @param ignoredException ignored exception + * @see Awaitility#ignoreExceptionsByDefault() + * @see Awaitility#ignoreExceptionByDefault(Class) + * @see ConditionFactory#ignoreExceptions() + * @see ConditionFactory#ignoreException(Class) + * @see awaitlity wiki + */ + @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); + }); + } + } + + /** + * 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/io/qameta/allure/awaitility/TemporalDuration.java b/allure-awaitility/src/main/java/io/qameta/allure/awaitility/TemporalDuration.java new file mode 100644 index 000000000..d549ee811 --- /dev/null +++ b/allure-awaitility/src/main/java/io/qameta/allure/awaitility/TemporalDuration.java @@ -0,0 +1,83 @@ +/* + * Copyright 2016-2024 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 java.time.Duration; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalField; +import java.time.temporal.UnsupportedTemporalTypeException; + +import static java.time.temporal.ChronoField.DAY_OF_MONTH; +import static java.time.temporal.ChronoField.HOUR_OF_DAY; +import static java.time.temporal.ChronoField.MILLI_OF_SECOND; +import static java.time.temporal.ChronoField.MINUTE_OF_HOUR; +import static java.time.temporal.ChronoField.MONTH_OF_YEAR; +import static java.time.temporal.ChronoField.SECOND_OF_MINUTE; +import static java.time.temporal.ChronoField.YEAR; + +/** + * Class helper for Duration printing purposes. + */ +public class TemporalDuration implements TemporalAccessor { + private static final Temporal BASE = LocalDateTime.of(0, 1, 1, 0, 0, 0, 0); + + private static final DateTimeFormatter DTF = new DateTimeFormatterBuilder().optionalStart() + .appendValue(YEAR) + .appendLiteral(" years ").optionalEnd() + .optionalStart().appendLiteral(' ').appendValue(MONTH_OF_YEAR).appendLiteral(" months ").optionalEnd() + .optionalStart().appendLiteral(' ').appendValue(DAY_OF_MONTH).appendLiteral(" days ").optionalEnd() + .optionalStart().appendLiteral(' ').appendValue(HOUR_OF_DAY).appendLiteral(" hours ").optionalEnd() + .optionalStart().appendLiteral(' ').appendValue(MINUTE_OF_HOUR).appendLiteral(" minutes ").optionalEnd() + .optionalStart().appendLiteral(' ').appendValue(SECOND_OF_MINUTE).appendLiteral(" seconds").optionalEnd() + .optionalStart().appendLiteral(' ').appendValue(MILLI_OF_SECOND).appendLiteral(" milliseconds") + .optionalEnd() + .toFormatter(); + + private final Duration duration; + private final Temporal temporal; + + TemporalDuration(final Duration duration) { + this.duration = duration; + this.temporal = duration.addTo(BASE); + } + + @Override + public boolean isSupported(final TemporalField field) { + return temporal.isSupported(field) && temporal.getLong(field) - BASE.getLong(field) != 0L; + } + + @Override + public long getLong(final TemporalField temporalField) { + if (!isSupported(temporalField)) { + throw new UnsupportedTemporalTypeException(temporalField.toString()); + } + return temporal.getLong(temporalField) - BASE.getLong(temporalField); + } + + @Override + public String toString() { + if (duration.compareTo(Duration.ofMillis(1)) < 0) { + return duration.toNanos() + " nanoseconds"; + } else { + return DTF.format(this).trim(); + } + } + +} 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 new file mode 100644 index 000000000..1a3bad814 --- /dev/null +++ b/allure-awaitility/src/test/java/io/qameta/allure/awaitility/ConditionListenersPositiveTest.java @@ -0,0 +1,181 @@ +/* + * Copyright 2016-2024 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.model.Status; +import io.qameta.allure.model.StepResult; +import io.qameta.allure.model.TestResult; +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; + +class ConditionListenersPositiveTest { + + @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
  • + */ + @TestFactory + Stream globalSettingsAwaitWoAliasCheckTopLevelPassedStep() { + final List testResult = runWithinTestContext(() -> { + final AtomicInteger atomicInteger = new AtomicInteger(0); + 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(); + + 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") + ) + ); + } + + /** + * 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')
  • + */ + @Test + void globalSettingsAwaitWithAliasCheckTopLevelPassedStep() { + final List testResult = runWithinTestContext(() -> { + final AtomicInteger atomicInteger = new AtomicInteger(0); + 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(); + assertEquals( + "Awaitility: Counter should be at least 3", + testResult.get(0).getSteps().get(0).getName(), + "Top level step has name with alias" + ); + } + + /** + * 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() { + final List testResult = runWithinTestContext(() -> { + final AtomicInteger atomicInteger = new AtomicInteger(0); + 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(); + + 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") + ) + ); + } + +} 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 new file mode 100644 index 000000000..5c169fd63 --- /dev/null +++ b/allure-awaitility/src/test/java/io/qameta/allure/awaitility/GlobalSettingsNegativeTest.java @@ -0,0 +1,127 @@ +/* + * Copyright 2016-2024 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.model.Status; +import io.qameta.allure.model.TestResult; +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) +class GlobalSettingsNegativeTest { + + @AfterEach + void reset() { + Awaitility.reset(); + } + + @BeforeEach + void setup() { + Awaitility.pollInSameThread(); + Awaitility.setDefaultConditionEvaluationListener(new AllureAwaitilityListener()); + } + + /** + * 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
  • + */ + @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" + ); + } + + /** + * 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
  • + */ + @TestFactory + Stream globalSettingsCheckAwaitWoAliasSecondLevelTimeoutStep() { + 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(); + + 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)) + ); + } + +} 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 new file mode 100644 index 000000000..52a6b50db --- /dev/null +++ b/allure-awaitility/src/test/java/io/qameta/allure/awaitility/GlobalSettingsPositiveTest.java @@ -0,0 +1,199 @@ +/* + * Copyright 2016-2024 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.model.Status; +import io.qameta.allure.model.StepResult; +import io.qameta.allure.model.TestResult; +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; + +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) +class GlobalSettingsPositiveTest { + + @AfterEach + void reset() { + Awaitility.reset(); + } + + @BeforeEach + void setup() { + Awaitility.pollInSameThread(); + Awaitility.setDefaultConditionEvaluationListener(new AllureAwaitilityListener()); + } + + /** + * 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
  • + */ + @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" + ) + ) + ); + } + + /** + * 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')
  • + */ + @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" + ); + } + + /** + * 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
  • + */ + @Test + Stream globalSettingsCheckAwaitWoAliasSecondLevelPassedSteps() { + 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") + ) + ); + } + +} 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 new file mode 100644 index 000000000..36a74a164 --- /dev/null +++ b/allure-awaitility/src/test/java/io/qameta/allure/awaitility/MultipleConditionsTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2016-2024 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.model.Status; +import io.qameta.allure.model.TestResult; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.*; + +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; + +public class MultipleConditionsTest { + + @AfterEach + void reset() { + Awaitility.reset(); + } + + @BeforeEach + void setup() { + Awaitility.pollInSameThread(); + Awaitility.setDefaultConditionEvaluationListener(new AllureAwaitilityListener()); + } + + @TestFactory + Stream bothAwaitilityStepsShouldAppearTest() { + final List testResult = runWithinTestContext(() -> { + await().with() + .alias("First waiting") + .until(() -> true); + 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())) + ) + ); + } + +} diff --git a/allure-bom/build.gradle.kts b/allure-bom/build.gradle.kts new file mode 100644 index 000000000..9593300aa --- /dev/null +++ b/allure-bom/build.gradle.kts @@ -0,0 +1,33 @@ +plugins { + `java-platform` +} + +description = "Allure Java (Bill of Materials)" + +dependencies { + constraints { + rootProject.subprojects.sorted() + .forEach { api("${it.group}:${it.name}:${it.version}") } + } +} + +tasks.withType(Jar::class) { + enabled = false +} + +configurations.archives { + artifacts.removeAll{ it.extension == "jar" } +} + +publishing.publications.named("maven") { + pom { + from(components["javaPlatform"]) + description.set("This Bill of Materials POM can be used to ease dependency management " + + "when referencing multiple Allure artifacts using Gradle or Maven.") + packaging = "pom" + withXml { + val filteredContent = asString().replace("\\s*compile".toRegex(), "") + asString().clear().append(filteredContent) + } + } +} diff --git a/allure-citrus/build.gradle.kts b/allure-citrus/build.gradle.kts index bd740f9a7..dbad67a9c 100644 --- a/allure-citrus/build.gradle.kts +++ b/allure-citrus/build.gradle.kts @@ -1,11 +1,8 @@ description = "Allure Citrus Integration" -val agent: Configuration by configurations.creating - val citrusVersion = "2.8.0" dependencies { - agent("org.aspectj:aspectjweaver") api(project(":allure-java-commons")) compileOnly("com.consol.citrus:citrus-core:$citrusVersion") testImplementation("com.consol.citrus:citrus-http:$citrusVersion") @@ -32,7 +29,4 @@ tasks.jar { tasks.test { useJUnitPlatform() exclude("**/samples/*") - doFirst { - jvmArgs("-javaagent:${agent.singleFile}") - } } 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 3db26c236..21f68bebe 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 @@ -1,5 +1,5 @@ /* - * Copyright 2019 Qameta Software OÜ + * Copyright 2016-2024 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. diff --git a/allure-citrus/src/test/java/io/qameta/allure/citrus/AllureCitrusTest.java b/allure-citrus/src/test/java/io/qameta/allure/citrus/AllureCitrusTest.java index a31fd6f00..7f29c0490 100644 --- a/allure-citrus/src/test/java/io/qameta/allure/citrus/AllureCitrusTest.java +++ b/allure-citrus/src/test/java/io/qameta/allure/citrus/AllureCitrusTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Qameta Software OÜ + * Copyright 2016-2024 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. diff --git a/allure-cucumber-jvm/src/main/java/io/qameta/allure/cucumberjvm/AllureCucumberJvm.java b/allure-cucumber-jvm/src/main/java/io/qameta/allure/cucumberjvm/AllureCucumberJvm.java deleted file mode 100644 index 2b6c036d9..000000000 --- a/allure-cucumber-jvm/src/main/java/io/qameta/allure/cucumberjvm/AllureCucumberJvm.java +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Copyright 2019 Qameta Software OÜ - * - * 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.cucumberjvm; - -import cucumber.runtime.StepDefinitionMatch; -import gherkin.I18n; -import gherkin.formatter.Formatter; -import gherkin.formatter.Reporter; -import gherkin.formatter.model.Background; -import gherkin.formatter.model.DataTableRow; -import gherkin.formatter.model.Examples; -import gherkin.formatter.model.Feature; -import gherkin.formatter.model.Match; -import gherkin.formatter.model.Result; -import gherkin.formatter.model.Scenario; -import gherkin.formatter.model.ScenarioOutline; -import gherkin.formatter.model.Step; -import gherkin.formatter.model.Tag; -import io.qameta.allure.Allure; -import io.qameta.allure.AllureLifecycle; -import io.qameta.allure.model.Status; -import io.qameta.allure.model.StatusDetails; -import io.qameta.allure.model.StepResult; -import io.qameta.allure.model.TestResult; -import io.qameta.allure.util.ResultsUtils; - -import java.io.ByteArrayInputStream; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.Objects; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Deque; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Allure plugin for Cucumber-JVM. - */ -@SuppressWarnings({ - "PMD.ExcessiveImports", - "ClassFanOutComplexity", - "ClassDataAbstractionCoupling", - "unused" -}) -public class AllureCucumberJvm implements Reporter, Formatter { - - private static final List SCENARIO_OUTLINE_KEYWORDS = Collections.synchronizedList(new ArrayList()); - - private static final String FAILED = "failed"; - private static final String PASSED = "passed"; - private static final String SKIPPED = "skipped"; - private static final String PENDING = "pending"; - - private static final String TXT_EXTENSION = ".txt"; - private static final String TEXT_PLAIN = "text/plain"; - - private final Map scenarioUuids = new ConcurrentHashMap<>(); - private final Deque gherkinSteps = new LinkedList<>(); - private final AllureLifecycle lifecycle; - private Feature currentFeature; - private boolean isNullMatch = true; - private Scenario currentScenario; - - - @SuppressWarnings("unused") - public AllureCucumberJvm() { - this(Allure.getLifecycle()); - } - - public AllureCucumberJvm(final AllureLifecycle lifecycle) { - this.lifecycle = lifecycle; - final List i18nList = I18n.getAll(); - i18nList.forEach(i18n -> SCENARIO_OUTLINE_KEYWORDS.addAll(i18n.keywords("scenario_outline"))); - } - - @Override - public void feature(final Feature feature) { - this.currentFeature = feature; - } - - @Override - public void before(final Match match, final Result result) { - new StepUtils(currentFeature, currentScenario).fireFixtureStep(match, result, true); - } - - @Override - public void after(final Match match, final Result result) { - new StepUtils(currentFeature, currentScenario).fireFixtureStep(match, result, false); - } - - @Override - public void startOfScenarioLifeCycle(final Scenario scenario) { - this.currentScenario = scenario; - - final Deque tags = new LinkedList<>(scenario.getTags()); - - if (SCENARIO_OUTLINE_KEYWORDS.contains(scenario.getKeyword())) { - synchronized (gherkinSteps) { - gherkinSteps.clear(); - } - } else { - tags.addAll(currentFeature.getTags()); - } - - final LabelBuilder labelBuilder = new LabelBuilder(currentFeature, scenario, tags); - - final String uuid = UUID.randomUUID().toString(); - scenarioUuids.put(scenario, uuid); - - final TestResult result = new TestResult() - .setUuid(uuid) - .setHistoryId(StepUtils.getHistoryId(scenario.getId())) - .setFullName(String.format("%s: %s", currentFeature.getName(), scenario.getName())) - .setName(scenario.getName()) - .setLabels(labelBuilder.getScenarioLabels()) - .setLinks(labelBuilder.getScenarioLinks()); - - if (!currentFeature.getDescription().isEmpty()) { - result.setDescription(currentFeature.getDescription()); - } - - lifecycle.scheduleTestCase(result); - lifecycle.startTestCase(uuid); - - } - - @Override - public void step(final Step step) { - synchronized (gherkinSteps) { - gherkinSteps.add(step); - } - } - - @Override - public void match(final Match match) { - final StepUtils stepUtils = new StepUtils(currentFeature, currentScenario); - if (match instanceof StepDefinitionMatch) { - isNullMatch = false; - final Step step = stepUtils.extractStep((StepDefinitionMatch) match); - synchronized (gherkinSteps) { - while (gherkinSteps.peek() != null && !stepUtils.isEqualSteps(step, gherkinSteps.peek())) { - stepUtils.fireCanceledStep(gherkinSteps.remove()); - } - if (stepUtils.isEqualSteps(step, gherkinSteps.peek())) { - gherkinSteps.remove(); - } - } - final StepResult stepResult = new StepResult(); - stepResult.setName(String.format("%s %s", step.getKeyword(), getStepName(step))) - .setStart(System.currentTimeMillis()); - - final String scenarioUuid = scenarioUuids.get(currentScenario); - lifecycle.startStep(scenarioUuid, stepUtils.getStepUuid(step), stepResult); - createDataTableAttachment(step.getRows()); - } - } - - @SuppressWarnings("PMD.NcssCount") - @Override - public void result(final Result result) { - if (!isNullMatch) { - final StatusDetails statusDetails = - ResultsUtils.getStatusDetails(result.getError()).orElse(new StatusDetails()); - final TagParser tagParser = new TagParser(currentFeature, currentScenario); - statusDetails - .setFlaky(tagParser.isFlaky()) - .setMuted(tagParser.isMuted()) - .setKnown(tagParser.isKnown()); - - final String scenarioUuid = scenarioUuids.get(currentScenario); - switch (result.getStatus()) { - case FAILED: - final Status status = ResultsUtils.getStatus(result.getError()) - .orElse(Status.FAILED); - lifecycle.updateStep(stepResult -> stepResult.setStatus(Status.FAILED)); - lifecycle.updateTestCase(scenarioUuid, scenarioResult -> - scenarioResult.setStatus(status) - .setStatusDetails(statusDetails)); - lifecycle.stopStep(); - break; - case PENDING: - lifecycle.updateStep(stepResult -> stepResult.setStatus(Status.SKIPPED)); - lifecycle.updateTestCase(scenarioUuid, scenarioResult -> - scenarioResult.setStatus(Status.SKIPPED) - .setStatusDetails(statusDetails)); - lifecycle.stopStep(); - break; - case SKIPPED: - lifecycle.updateStep(stepResult -> stepResult.setStatus(Status.SKIPPED)); - lifecycle.stopStep(); - break; - case PASSED: - lifecycle.updateStep(stepResult -> stepResult.setStatus(Status.PASSED)); - lifecycle.stopStep(); - lifecycle.updateTestCase(scenarioUuid, scenarioResult -> - scenarioResult.setStatus(Status.PASSED) - .setStatusDetails(statusDetails)); - break; - default: - break; - } - isNullMatch = true; - } - } - - @Override - public void endOfScenarioLifeCycle(final Scenario scenario) { - final StepUtils stepUtils = new StepUtils(currentFeature, currentScenario); - synchronized (gherkinSteps) { - while (gherkinSteps.peek() != null) { - stepUtils.fireCanceledStep(gherkinSteps.remove()); - } - } - final String scenarioUuid = scenarioUuids.remove(scenario); - lifecycle.stopTestCase(scenarioUuid); - lifecycle.writeTestCase(scenarioUuid); - } - - public String getStepName(final Step step) { - return step.getName(); - } - - private void createDataTableAttachment(final List dataTableRows) { - final StringBuilder dataTableCsv = new StringBuilder(); - - if (dataTableRows != null && !dataTableRows.isEmpty()) { - dataTableRows.forEach(dataTableRow -> { - dataTableCsv.append(String.join("\t", dataTableRow.getCells())); - dataTableCsv.append('\n'); - }); - - final String attachmentSource = lifecycle - .prepareAttachment("Data table", "text/tab-separated-values", "csv"); - lifecycle.writeAttachment(attachmentSource, - new ByteArrayInputStream(dataTableCsv.toString().getBytes(Charset.forName("UTF-8")))); - } - } - - @Override - public void embedding(final String string, final byte[] bytes) { - lifecycle.addAttachment("Screenshot", null, null, new ByteArrayInputStream(bytes)); - } - - @Override - public void write(final String string) { - lifecycle.addAttachment( - "Text output", - TEXT_PLAIN, - TXT_EXTENSION, - Objects.toString(string).getBytes(StandardCharsets.UTF_8) - ); - } - - @Override - public void syntaxError(final String state, final String event, - final List legalEvents, final String uri, final Integer line) { - //Nothing to do with Allure - } - - @Override - public void uri(final String uri) { - //Nothing to do with Allure - } - - @Override - public void scenarioOutline(final ScenarioOutline so) { - //Nothing to do with Allure - } - - @Override - public void examples(final Examples exmpls) { - //Nothing to do with Allure - } - - - @Override - public void background(final Background b) { - //Nothing to do with Allure - } - - @Override - public void scenario(final Scenario scnr) { - //Nothing to do with Allure - } - - @Override - public void done() { - //Nothing to do with Allure - } - - @Override - public void close() { - //Nothing to do with Allure - } - - @Override - public void eof() { - //Nothing to do with Allure - - } - -} diff --git a/allure-cucumber-jvm/src/main/java/io/qameta/allure/cucumberjvm/StepUtils.java b/allure-cucumber-jvm/src/main/java/io/qameta/allure/cucumberjvm/StepUtils.java deleted file mode 100644 index 99c262b38..000000000 --- a/allure-cucumber-jvm/src/main/java/io/qameta/allure/cucumberjvm/StepUtils.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2019 Qameta Software OÜ - * - * 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.cucumberjvm; - -import cucumber.runtime.CucumberException; -import cucumber.runtime.StepDefinitionMatch; -import gherkin.formatter.model.Feature; -import gherkin.formatter.model.Match; -import gherkin.formatter.model.Result; -import gherkin.formatter.model.Scenario; -import gherkin.formatter.model.Step; -import io.qameta.allure.Allure; -import io.qameta.allure.AllureLifecycle; -import io.qameta.allure.model.Status; -import io.qameta.allure.model.StatusDetails; -import io.qameta.allure.model.StepResult; -import io.qameta.allure.util.ResultsUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.lang.reflect.Field; -import java.util.Objects; - -import static io.qameta.allure.util.ResultsUtils.md5; - -/** - * Step utils. - */ -class StepUtils { - private static final Logger LOG = LoggerFactory.getLogger(StepUtils.class); - private static final String FAILED = "failed"; - - private final AllureLifecycle lifecycle; - private final Feature feature; - private final Scenario scenario; - - StepUtils(final Feature feature, final Scenario scenario) { - this.lifecycle = Allure.getLifecycle(); - this.feature = feature; - this.scenario = scenario; - } - - protected Step extractStep(final StepDefinitionMatch match) { - try { - final Field step = match.getClass().getDeclaredField("step"); - step.setAccessible(true); - return (Step) step.get(match); - } catch (ReflectiveOperationException e) { - //shouldn't ever happen - LOG.error(e.getMessage(), e); - throw new CucumberException(e); - } - } - - protected boolean isEqualSteps(final Step step, final Step gherkinStep) { - return Objects.equals(step.getLine(), gherkinStep.getLine()); - } - - protected void fireCanceledStep(final Step unimplementedStep) { - final StepResult stepResult = new StepResult(); - stepResult.setName(unimplementedStep.getName()) - .setStart(System.currentTimeMillis()) - .setStop(System.currentTimeMillis()) - .setStatus(Status.SKIPPED) - .setStatusDetails(new StatusDetails().setMessage("Unimplemented step")); - lifecycle.startStep(scenario.getId(), getStepUuid(unimplementedStep), stepResult); - lifecycle.stopStep(getStepUuid(unimplementedStep)); - - final StatusDetails statusDetails = new StatusDetails(); - final TagParser tagParser = new TagParser(feature, scenario); - statusDetails - .setFlaky(tagParser.isFlaky()) - .setMuted(tagParser.isMuted()) - .setKnown(tagParser.isKnown()); - lifecycle.updateTestCase(scenario.getId(), scenarioResult -> - scenarioResult.setStatus(Status.SKIPPED) - .setStatusDetails(statusDetails - .setMessage("Unimplemented steps were found"))); - } - - protected String getStepUuid(final Step step) { - return feature.getId() + scenario.getId() + step.getName() + step.getLine(); - } - - protected static String getHistoryId(final String id) { - return md5(id); - } - - protected void fireFixtureStep(final Match match, final Result result, final boolean isBefore) { - final String uuid = md5(match.getLocation()); - final StepResult stepResult = new StepResult() - .setName(match.getLocation()) - .setStatus(Status.fromValue(result.getStatus())) - .setStart(System.currentTimeMillis() - result.getDuration()) - .setStop(System.currentTimeMillis()); - if (FAILED.equals(result.getStatus())) { - final StatusDetails statusDetails = ResultsUtils.getStatusDetails(result.getError()).get(); - stepResult.setStatusDetails(statusDetails); - if (isBefore) { - final TagParser tagParser = new TagParser(feature, scenario); - statusDetails - .setMessage("Before is failed: " + result.getError().getLocalizedMessage()) - .setFlaky(tagParser.isFlaky()) - .setMuted(tagParser.isMuted()) - .setKnown(tagParser.isKnown()); - lifecycle.updateTestCase(scenario.getId(), scenarioResult -> - scenarioResult.setStatus(Status.SKIPPED) - .setStatusDetails(statusDetails)); - } - } - lifecycle.startStep(scenario.getId(), uuid, stepResult); - lifecycle.stopStep(uuid); - } -} diff --git a/allure-cucumber-jvm/src/test/java/io/qameta/allure/cucumberjvm/AllureCucumberJvmTest.java b/allure-cucumber-jvm/src/test/java/io/qameta/allure/cucumberjvm/AllureCucumberJvmTest.java deleted file mode 100644 index 5b63d63ba..000000000 --- a/allure-cucumber-jvm/src/test/java/io/qameta/allure/cucumberjvm/AllureCucumberJvmTest.java +++ /dev/null @@ -1,439 +0,0 @@ -/* - * Copyright 2019 Qameta Software OÜ - * - * 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.cucumberjvm; - -import cucumber.api.testng.FeatureResultListener; -import cucumber.runtime.ClassFinder; -import cucumber.runtime.Runtime; -import cucumber.runtime.RuntimeOptions; -import cucumber.runtime.io.MultiLoader; -import cucumber.runtime.io.ResourceLoader; -import cucumber.runtime.io.ResourceLoaderClassFinder; -import cucumber.runtime.model.CucumberFeature; -import io.github.glytching.junit.extension.system.SystemProperty; -import io.github.glytching.junit.extension.system.SystemPropertyExtension; -import io.qameta.allure.AllureLifecycle; -import io.qameta.allure.Issue; -import io.qameta.allure.model.Attachment; -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; -import io.qameta.allure.model.TestResult; -import io.qameta.allure.test.AllureFeatures; -import io.qameta.allure.test.AllureResults; -import io.qameta.allure.test.AllureResultsWriterStub; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import static java.lang.Thread.currentThread; -import static java.util.Objects.nonNull; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.tuple; - -/** - * @author charlie (Dmitry Baev). - */ -@SuppressWarnings("unchecked") -class AllureCucumberJvmTest { - - @AllureFeatures.Base - @Test - void shouldSetName() { - final AllureResults results = runFeature("features/simple.feature"); - - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getName) - .containsExactlyInAnyOrder("Add a to b"); - - } - - @AllureFeatures.PassedTests - @Test - void shouldSetStatus() { - final AllureResults results = runFeature("features/simple.feature"); - - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.PASSED); - } - - @AllureFeatures.FailedTests - @Test - void shouldSetFailedStatus() { - final AllureResults results = runFeature("features/failed.feature"); - - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.FAILED); - } - - @AllureFeatures.FailedTests - @Test - void shouldSetStatusDetails() { - final AllureResults results = runFeature("features/failed.feature"); - - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getStatusDetails) - .extracting(StatusDetails::getMessage) - .containsExactlyInAnyOrder("expected: <15> but was: <123>"); - } - - @AllureFeatures.BrokenTests - @Test - void shouldSetBrokenStatus() { - final AllureResults results = runFeature("features/broken.feature"); - - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.BROKEN); - } - - @AllureFeatures.Stages - @Test - void shouldSetStage() { - final AllureResults results = runFeature("features/simple.feature"); - - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getStage) - .containsExactlyInAnyOrder(Stage.FINISHED); - } - - @AllureFeatures.Timings - @Test - void shouldSetStart() { - final long before = Instant.now().toEpochMilli(); - final AllureResults results = runFeature("features/simple.feature"); - final long after = Instant.now().toEpochMilli(); - - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getStart) - .allMatch(v -> v >= before && v <= after); - } - - @AllureFeatures.Timings - @Test - void shouldSetStop() { - final long before = Instant.now().toEpochMilli(); - final AllureResults results = runFeature("features/simple.feature"); - final long after = Instant.now().toEpochMilli(); - - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getStop) - .allMatch(v -> v >= before && v <= after); - } - - @AllureFeatures.FullName - @Test - void shouldSetFullName() { - final AllureResults results = runFeature("features/simple.feature"); - - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getFullName) - .containsExactlyInAnyOrder("Simple feature: Add a to b"); - } - - @AllureFeatures.Descriptions - @Test - void shouldSetDescription() { - final AllureResults results = runFeature("features/description.feature"); - - final String expected = "\nThis is description for current feature.\n" - + "It should appear on each scenario in report"; - - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getDescription) - .containsExactlyInAnyOrder( - expected, - expected - ); - } - - @AllureFeatures.Attachments - @Test - void shouldAddDataTableAttachment() { - final AllureResults results = runFeature("features/datatable.feature"); - - final List attachments = results.getTestResults().stream() - .map(TestResult::getSteps) - .flatMap(Collection::stream) - .map(StepResult::getAttachments) - .flatMap(Collection::stream) - .collect(Collectors.toList()); - - assertThat(attachments) - .extracting(Attachment::getName, Attachment::getType) - .containsExactlyInAnyOrder( - tuple("Data table", "text/tab-separated-values") - ); - - final Attachment dataTableAttachment = attachments.iterator().next(); - final Map attachmentFiles = results.getAttachments(); - assertThat(attachmentFiles) - .containsKeys(dataTableAttachment.getSource()); - - final byte[] bytes = attachmentFiles.get(dataTableAttachment.getSource()); - final String attachmentContent = new String(bytes, StandardCharsets.UTF_8); - - assertThat(attachmentContent) - .isEqualTo("name\tlogin\temail\n" + - "Viktor\tclicman\tclicman@ya.ru\n" + - "Viktor2\tclicman2\tclicman2@ya.ru\n" - ); - - } - - @AllureFeatures.Attachments - @Test - void shouldAddAttachments() { - final AllureResults results = runFeature("features/attachments.feature"); - - final List attachments = results.getTestResults().stream() - .map(TestResult::getSteps) - .flatMap(Collection::stream) - .map(StepResult::getAttachments) - .flatMap(Collection::stream) - .collect(Collectors.toList()); - - assertThat(attachments) - .extracting(Attachment::getName, Attachment::getType) - .containsExactlyInAnyOrder( - tuple("Text output", "text/plain"), - tuple("Screenshot", null) - ); - - final List attachmentContents = results.getAttachments().values().stream() - .map(bytes -> new String(bytes, StandardCharsets.UTF_8)) - .collect(Collectors.toList()); - - assertThat(attachmentContents) - .containsExactlyInAnyOrder("text attachment", "image attachment"); - } - - @AllureFeatures.Steps - @Disabled("unsupported") - @Test - void shouldAddBackgroundSteps() { - final AllureResults results = runFeature("features/background.feature"); - - final List testResults = results.getTestResults(); - - assertThat(testResults) - .hasSize(1) - .flatExtracting(TestResult::getSteps) - .extracting(StepResult::getName) - .containsExactly( - "Given cat is sad", - "And cat is murmur", - "When Pet the cat", - "Then Cat is happy" - ); - } - - @AllureFeatures.Parameters - @Disabled("unsupported") - @Test - void shouldAddParametersFromExamples() { - final AllureResults results = runFeature("features/examples.feature"); - - final List testResults = results.getTestResults(); - - assertThat(testResults) - .hasSize(2); - - assertThat(testResults) - .flatExtracting(TestResult::getParameters) - .extracting(Parameter::getName, Parameter::getValue) - .containsExactlyInAnyOrder( - tuple("a", "1"), tuple("b", "3"), tuple("result", "4"), - tuple("a", "2"), tuple("b", "4"), tuple("result", "6") - ); - - } - - @AllureFeatures.MarkerAnnotations - @Test - void shouldAddTags() { - final AllureResults results = runFeature("features/tags.feature"); - - final List testResults = results.getTestResults(); - - assertThat(testResults) - .flatExtracting(TestResult::getLabels) - .extracting(Label::getName, Label::getValue) - .contains( - tuple("tag", "FeatureTag"), - tuple("tag", "good") - ); - } - - @AllureFeatures.Links - @ExtendWith(SystemPropertyExtension.class) - @SystemProperty(name = "allure.link.issue.pattern", value = "https://example.org/issue/{}") - @SystemProperty(name = "allure.link.tms.pattern", value = "https://example.org/tms/{}") - @Test - void shouldAddLinks() { - final AllureResults results = runFeature("features/tags.feature"); - - final List testResults = results.getTestResults(); - - assertThat(testResults) - .flatExtracting(TestResult::getLinks) - .extracting(Link::getName, Link::getType, Link::getUrl) - .contains( - tuple("OAT-4444", "tms", "https://example.org/tms/OAT-4444"), - tuple("BUG-22400", "issue", "https://example.org/issue/BUG-22400") - ); - } - - @AllureFeatures.MarkerAnnotations - @Test - void shouldAddBddLabels() { - final AllureResults results = runFeature("features/tags.feature"); - - final List testResults = results.getTestResults(); - - assertThat(testResults) - .flatExtracting(TestResult::getLabels) - .extracting(Label::getName, Label::getValue) - .contains( - tuple("feature", "Test Simple Scenarios"), - tuple("story", "Add a to b") - ); - } - - @AllureFeatures.Timeline - @Test - void shouldThreadHostLabels() { - final AllureResults results = runFeature("features/tags.feature"); - - final List testResults = results.getTestResults(); - - assertThat(testResults) - .flatExtracting(TestResult::getLabels) - .extracting(Label::getName) - .contains("host", "thread"); - } - - @AllureFeatures.MarkerAnnotations - @Test - void shouldCommonLabels() { - final AllureResults results = runFeature("features/tags.feature"); - - final List testResults = results.getTestResults(); - - assertThat(testResults) - .flatExtracting(TestResult::getLabels) - .extracting(Label::getName, Label::getValue) - .contains( - tuple("package", "Test Simple Scenarios"), - tuple("suite", "Test Simple Scenarios"), - tuple("testClass", "Add a to b") - ); - } - - @AllureFeatures.NotImplementedTests - @Test - void shouldProcessNotImplementedScenario() { - final AllureResults results = runFeature("features/undefined.feature"); - - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder((Status) null); - } - - @AllureFeatures.Base - @Disabled("unsupported") - @Test - void shouldSupportDryRun() { - final AllureResults results = runFeature("features/simple.feature", "--dry-run"); - - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getName, TestResult::getStatus) - .containsExactlyInAnyOrder( - tuple("Add a to b", Status.SKIPPED) - ); - } - - @AllureFeatures.Base - @Issue("173") - @Issue("164") - @Test - void shouldUseUuid() { - final AllureResults results = runFeature("features/simple.feature"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getUuid) - .allMatch(uuid -> nonNull(uuid) && uuid.matches("[\\-a-z0-9]+"), "UUID"); - } - - private AllureResults runFeature(final String featureResource, - final String... moreOptions) { - final AllureResultsWriterStub writer = new AllureResultsWriterStub(); - final AllureLifecycle lifecycle = new AllureLifecycle(writer); - final AllureCucumberJvm cucumberJvm = new AllureCucumberJvm(lifecycle); - final ClassLoader classLoader = currentThread().getContextClassLoader(); - final ResourceLoader resourceLoader = new MultiLoader(classLoader); - final ClassFinder classFinder = new ResourceLoaderClassFinder(resourceLoader, classLoader); - final List opts = new ArrayList<>(Arrays.asList( - "--glue", "io.qameta.allure.cucumberjvm.samples", - "--plugin", "null", - "src/test/resources/" + featureResource - )); - opts.addAll(Arrays.asList(moreOptions)); - final RuntimeOptions options = new RuntimeOptions(opts); - final Runtime runtime = new Runtime(resourceLoader, classFinder, classLoader, options); - - options.addPlugin(cucumberJvm); - - final FeatureResultListener resultListener = new FeatureResultListener( - options.reporter(classLoader), - options.isStrict() - ); - final List features = options.cucumberFeatures(resourceLoader); - features.forEach(cucumberFeature -> cucumberFeature.run( - options.formatter(classLoader), - resultListener, - runtime) - ); - return writer; - } -} diff --git a/allure-cucumber-jvm/src/test/resources/features/undefined.feature b/allure-cucumber-jvm/src/test/resources/features/undefined.feature deleted file mode 100644 index caa0b507b..000000000 --- a/allure-cucumber-jvm/src/test/resources/features/undefined.feature +++ /dev/null @@ -1,4 +0,0 @@ -Feature: Simple feature - - Scenario: Step is not implemented - Given hello my friend diff --git a/allure-cucumber2-jvm/src/main/java/io/qameta/allure/cucumber2jvm/AllureCucumber2Jvm.java b/allure-cucumber2-jvm/src/main/java/io/qameta/allure/cucumber2jvm/AllureCucumber2Jvm.java deleted file mode 100644 index 3be912369..000000000 --- a/allure-cucumber2-jvm/src/main/java/io/qameta/allure/cucumber2jvm/AllureCucumber2Jvm.java +++ /dev/null @@ -1,409 +0,0 @@ -/* - * Copyright 2019 Qameta Software OÜ - * - * 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.cucumber2jvm; - -import cucumber.api.HookType; -import cucumber.api.PendingException; -import cucumber.api.Result; -import cucumber.api.TestCase; -import cucumber.api.TestStep; -import cucumber.api.event.EmbedEvent; -import cucumber.api.event.EventHandler; -import cucumber.api.event.EventPublisher; -import cucumber.api.event.TestCaseFinished; -import cucumber.api.event.TestCaseStarted; -import cucumber.api.event.TestSourceRead; -import cucumber.api.event.TestStepFinished; -import cucumber.api.event.TestStepStarted; -import cucumber.api.event.WriteEvent; -import cucumber.api.formatter.Formatter; -import cucumber.runner.UnskipableStep; -import gherkin.ast.Examples; -import gherkin.ast.Feature; -import gherkin.ast.ScenarioDefinition; -import gherkin.ast.ScenarioOutline; -import gherkin.ast.TableCell; -import gherkin.pickles.PickleCell; -import gherkin.pickles.PickleRow; -import gherkin.pickles.PickleTable; -import gherkin.pickles.PickleTag; -import io.qameta.allure.Allure; -import io.qameta.allure.AllureLifecycle; -import io.qameta.allure.model.FixtureResult; -import io.qameta.allure.model.Parameter; -import io.qameta.allure.model.Status; -import io.qameta.allure.model.StatusDetails; -import io.qameta.allure.model.StepResult; -import io.qameta.allure.model.TestResult; -import io.qameta.allure.model.TestResultContainer; - -import java.io.ByteArrayInputStream; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.Deque; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.UUID; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import static io.qameta.allure.util.ResultsUtils.getStatus; -import static io.qameta.allure.util.ResultsUtils.getStatusDetails; -import static io.qameta.allure.util.ResultsUtils.md5; - -/** - * Allure plugin for Cucumber JVM 2.0. - */ -@SuppressWarnings({ - "PMD.ExcessiveImports", - "ClassFanOutComplexity", "ClassDataAbstractionCoupling" -}) -public class AllureCucumber2Jvm implements Formatter { - - private final AllureLifecycle lifecycle; - - private final Map scenarioUuids = new HashMap<>(); - - private final CucumberSourceUtils cucumberSourceUtils = new CucumberSourceUtils(); - private Feature currentFeature; - private String currentFeatureFile; - private TestCase currentTestCase; - private String currentContainer; - private boolean forbidTestCaseStatusChange; - - private final EventHandler featureStartedHandler = this::handleFeatureStartedHandler; - private final EventHandler caseStartedHandler = this::handleTestCaseStarted; - private final EventHandler caseFinishedHandler = this::handleTestCaseFinished; - private final EventHandler stepStartedHandler = this::handleTestStepStarted; - private final EventHandler stepFinishedHandler = this::handleTestStepFinished; - private final EventHandler writeEventHandler = this::handleWriteEvent; - private final EventHandler embedEventHandler = this::handleEmbedEvent; - - private static final String TXT_EXTENSION = ".txt"; - private static final String TEXT_PLAIN = "text/plain"; - - @SuppressWarnings("unused") - public AllureCucumber2Jvm() { - this(Allure.getLifecycle()); - } - - public AllureCucumber2Jvm(final AllureLifecycle lifecycle) { - this.lifecycle = lifecycle; - } - - @Override - public void setEventPublisher(final EventPublisher publisher) { - publisher.registerHandlerFor(TestSourceRead.class, featureStartedHandler); - - publisher.registerHandlerFor(TestCaseStarted.class, caseStartedHandler); - publisher.registerHandlerFor(TestCaseFinished.class, caseFinishedHandler); - - publisher.registerHandlerFor(TestStepStarted.class, stepStartedHandler); - publisher.registerHandlerFor(TestStepFinished.class, stepFinishedHandler); - - publisher.registerHandlerFor(WriteEvent.class, writeEventHandler); - publisher.registerHandlerFor(EmbedEvent.class, embedEventHandler); - } - - /* - Event Handlers - */ - - private void handleFeatureStartedHandler(final TestSourceRead event) { - cucumberSourceUtils.addTestSourceReadEvent(event.uri, event); - } - - private void handleTestCaseStarted(final TestCaseStarted event) { - currentTestCase = event.testCase; - currentFeatureFile = currentTestCase.getUri(); - currentFeature = cucumberSourceUtils.getFeature(currentFeatureFile); - currentContainer = UUID.randomUUID().toString(); - forbidTestCaseStatusChange = false; - - - final Deque tags = new LinkedList<>(currentTestCase.getTags()); - - final LabelBuilder labelBuilder = new LabelBuilder(currentFeature, currentTestCase, tags); - - final String name = currentTestCase.getName(); - final String featureName = currentFeature.getName(); - - final TestResult result = new TestResult() - .setUuid(getTestCaseUuid(currentTestCase)) - .setHistoryId(getHistoryId(currentTestCase)) - .setFullName(featureName + ": " + name) - .setName(name) - .setLabels(labelBuilder.getScenarioLabels()) - .setLinks(labelBuilder.getScenarioLinks()); - - final ScenarioDefinition scenarioDefinition = - cucumberSourceUtils.getScenarioDefinition(currentFeatureFile, currentTestCase.getLine()); - if (scenarioDefinition instanceof ScenarioOutline) { - result.setParameters( - getExamplesAsParameters((ScenarioOutline) scenarioDefinition) - ); - } - - if (currentFeature.getDescription() != null && !currentFeature.getDescription().isEmpty()) { - result.setDescription(currentFeature.getDescription()); - } - - final TestResultContainer resultContainer = new TestResultContainer() - .setName(String.format("%s: %s", scenarioDefinition.getKeyword(), scenarioDefinition.getName())) - .setUuid(getTestContainerUuid()) - .setChildren(Collections.singletonList(getTestCaseUuid(currentTestCase))); - - lifecycle.scheduleTestCase(result); - lifecycle.startTestContainer(getTestContainerUuid(), resultContainer); - lifecycle.startTestCase(getTestCaseUuid(currentTestCase)); - } - - private void handleTestCaseFinished(final TestCaseFinished event) { - - final String uuid = getTestCaseUuid(event.testCase); - final Optional details = getStatusDetails(event.result.getError()); - details.ifPresent(statusDetails -> lifecycle.updateTestCase( - uuid, - testResult -> testResult.setStatusDetails(statusDetails) - )); - lifecycle.stopTestCase(uuid); - lifecycle.stopTestContainer(getTestContainerUuid()); - lifecycle.writeTestCase(uuid); - lifecycle.writeTestContainer(getTestContainerUuid()); - } - - private void handleTestStepStarted(final TestStepStarted event) { - if (!event.testStep.isHook()) { - final String stepKeyword = Optional.ofNullable( - cucumberSourceUtils.getKeywordFromSource(currentFeatureFile, event.testStep.getStepLine()) - ).orElse("UNDEFINED"); - - final StepResult stepResult = new StepResult() - .setName(String.format("%s %s", stepKeyword, event.testStep.getPickleStep().getText())) - .setStart(System.currentTimeMillis()); - - lifecycle.startStep(getTestCaseUuid(currentTestCase), getStepUuid(event.testStep), stepResult); - - event.testStep.getStepArgument().stream() - .filter(PickleTable.class::isInstance) - .findFirst() - .ifPresent(table -> createDataTableAttachment((PickleTable) table)); - } else if (event.testStep instanceof UnskipableStep) { - initHook((UnskipableStep) event.testStep); - } - } - - private void initHook(final UnskipableStep hook) { - - final FixtureResult hookResult = new FixtureResult() - .setName(hook.getCodeLocation()) - .setStart(System.currentTimeMillis()); - - if (hook.getHookType() == HookType.Before) { - lifecycle.startPrepareFixture(getTestContainerUuid(), getHookStepUuid(hook), hookResult); - } else { - lifecycle.startTearDownFixture(getTestContainerUuid(), getHookStepUuid(hook), hookResult); - } - - } - - private void handleTestStepFinished(final TestStepFinished event) { - if (event.testStep.isHook() && event.testStep instanceof UnskipableStep) { - handleHookStep(event); - } else { - handlePickleStep(event); - } - } - - private void handleWriteEvent(final WriteEvent event) { - lifecycle.addAttachment( - "Text output", - TEXT_PLAIN, - TXT_EXTENSION, - Objects.toString(event.text).getBytes(StandardCharsets.UTF_8) - ); - } - - private void handleEmbedEvent(final EmbedEvent event) { - lifecycle.addAttachment("Screenshot", null, null, new ByteArrayInputStream(event.data)); - } - - /* - Utility Methods - */ - - private String getTestContainerUuid() { - return currentContainer; - } - - private String getTestCaseUuid(final TestCase testCase) { - return scenarioUuids.computeIfAbsent(getHistoryId(testCase), it -> UUID.randomUUID().toString()); - } - - private String getStepUuid(final TestStep step) { - return currentFeature.getName() + getTestCaseUuid(currentTestCase) - + step.getPickleStep().getText() + step.getStepLine(); - } - - private String getHookStepUuid(final TestStep step) { - return currentFeature.getName() + getTestCaseUuid(currentTestCase) - + step.getHookType().toString() + step.getCodeLocation(); - } - - private String getHistoryId(final TestCase testCase) { - final String testCaseLocation = testCase.getUri() + ":" + testCase.getLine(); - return md5(testCaseLocation); - } - - private Status translateTestCaseStatus(final Result testCaseResult) { - switch (testCaseResult.getStatus()) { - case FAILED: - return getStatus(testCaseResult.getError()) - .orElse(Status.FAILED); - case PASSED: - return Status.PASSED; - case SKIPPED: - case PENDING: - return Status.SKIPPED; - case AMBIGUOUS: - case UNDEFINED: - default: - return null; - } - } - - private List getExamplesAsParameters(final ScenarioOutline scenarioOutline) { - final int gap = 2; - final Optional examplesBlock = scenarioOutline.getExamples().stream() - .filter(e -> currentTestCase.getLine() >= e.getLocation().getLine() + gap) - .filter(e -> currentTestCase.getLine() < e.getLocation().getLine() + e.getTableBody().size() + gap) - .findFirst(); - - if (examplesBlock.isPresent()) { - final Examples examples = examplesBlock.get(); - final int rowIndex = currentTestCase.getLine() - examples.getLocation().getLine() - gap; - final List names = examples.getTableHeader().getCells(); - final List values = examples.getTableBody().get(rowIndex).getCells(); - return IntStream.range(0, examplesBlock.get().getTableHeader().getCells().size()).mapToObj(index -> { - final String name = names.get(index).getValue(); - final String value = values.get(index).getValue(); - return new Parameter().setName(name).setValue(value); - }).collect(Collectors.toList()); - } - return Collections.emptyList(); - } - - private void createDataTableAttachment(final PickleTable pickleTable) { - final List rows = pickleTable.getRows(); - - final StringBuilder dataTableCsv = new StringBuilder(); - if (!rows.isEmpty()) { - rows.forEach(dataTableRow -> { - dataTableCsv.append( - dataTableRow.getCells().stream() - .map(PickleCell::getValue) - .collect(Collectors.joining("\t")) - ); - dataTableCsv.append('\n'); - }); - - final String attachmentSource = lifecycle - .prepareAttachment("Data table", "text/tab-separated-values", "csv"); - lifecycle.writeAttachment(attachmentSource, - new ByteArrayInputStream(dataTableCsv.toString().getBytes(Charset.forName("UTF-8")))); - } - } - - private void handleHookStep(final TestStepFinished event) { - final String uuid = getHookStepUuid(event.testStep); - final FixtureResult fixtureResult = new FixtureResult().setStatus(translateTestCaseStatus(event.result)); - - if (!Status.PASSED.equals(fixtureResult.getStatus())) { - final TestResult testResult = new TestResult().setStatus(translateTestCaseStatus(event.result)); - final StatusDetails statusDetails = getStatusDetails(event.result.getError()).get(); - - statusDetails.setMessage(event.testStep.getHookType() - .name() + " is failed: " + event.result.getError().getLocalizedMessage()); - - if (event.testStep.getHookType() == HookType.Before) { - final TagParser tagParser = new TagParser(currentFeature, currentTestCase); - statusDetails - .setFlaky(tagParser.isFlaky()) - .setMuted(tagParser.isMuted()) - .setKnown(tagParser.isKnown()); - testResult.setStatus(Status.SKIPPED); - updateTestCaseStatus(testResult.getStatus()); - forbidTestCaseStatusChange = true; - } else { - testResult.setStatus(Status.BROKEN); - updateTestCaseStatus(testResult.getStatus()); - } - fixtureResult.setStatusDetails(statusDetails); - } - - lifecycle.updateFixture(uuid, result -> result.setStatus(fixtureResult.getStatus()) - .setStatusDetails(fixtureResult.getStatusDetails())); - lifecycle.stopFixture(uuid); - } - - private void handlePickleStep(final TestStepFinished event) { - - final Status stepStatus = translateTestCaseStatus(event.result); - final StatusDetails statusDetails; - if (event.result.getStatus() == Result.Type.UNDEFINED) { - updateTestCaseStatus(Status.PASSED); - - statusDetails = - getStatusDetails(new PendingException("TODO: implement me")) - .orElse(new StatusDetails()); - lifecycle.updateTestCase(getTestCaseUuid(currentTestCase), scenarioResult -> - scenarioResult - .setStatusDetails(statusDetails)); - } else { - statusDetails = - getStatusDetails(event.result.getError()) - .orElse(new StatusDetails()); - updateTestCaseStatus(stepStatus); - } - - - if (!Status.PASSED.equals(stepStatus) && stepStatus != null) { - forbidTestCaseStatusChange = true; - } - - final TagParser tagParser = new TagParser(currentFeature, currentTestCase); - statusDetails - .setFlaky(tagParser.isFlaky()) - .setMuted(tagParser.isMuted()) - .setKnown(tagParser.isKnown()); - - lifecycle.updateStep(getStepUuid(event.testStep), - stepResult -> stepResult.setStatus(stepStatus).setStatusDetails(statusDetails)); - lifecycle.stopStep(getStepUuid(event.testStep)); - } - - private void updateTestCaseStatus(final Status status) { - if (!forbidTestCaseStatusChange) { - lifecycle.updateTestCase(getTestCaseUuid(currentTestCase), - result -> result.setStatus(status)); - } - } -} diff --git a/allure-cucumber2-jvm/src/main/java/io/qameta/allure/cucumber2jvm/CucumberSourceUtils.java b/allure-cucumber2-jvm/src/main/java/io/qameta/allure/cucumber2jvm/CucumberSourceUtils.java deleted file mode 100644 index 519db53f6..000000000 --- a/allure-cucumber2-jvm/src/main/java/io/qameta/allure/cucumber2jvm/CucumberSourceUtils.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright 2019 Qameta Software OÜ - * - * 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.cucumber2jvm; - -import cucumber.api.event.TestSourceRead; - -import gherkin.Parser; -import gherkin.AstBuilder; -import gherkin.TokenMatcher; -import gherkin.ParserException; -import gherkin.GherkinDialect; -import gherkin.GherkinDialectProvider; - -import gherkin.ast.GherkinDocument; -import gherkin.ast.Feature; -import gherkin.ast.ScenarioDefinition; -import gherkin.ast.Node; -import gherkin.ast.ScenarioOutline; -import gherkin.ast.TableRow; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.HashMap; -import java.util.Map; -import java.util.stream.IntStream; - -/** - * Parts of package-private cucumber.runtime.formatter.TestSourcesModel needed for Allure 2 adapter. - */ -public final class CucumberSourceUtils { - - private static final Logger LOGGER = LoggerFactory.getLogger(CucumberSourceUtils.class); - - private final Map pathToReadEventMap = new HashMap<>(); - private final Map pathToAstMap = new HashMap<>(); - private final Map> pathToNodeMap = new HashMap<>(); - - public void addTestSourceReadEvent(final String path, final TestSourceRead event) { - pathToReadEventMap.put(path, event); - } - - public Feature getFeature(final String path) { - if (!pathToAstMap.containsKey(path)) { - parseGherkinSource(path); - } - if (pathToAstMap.containsKey(path)) { - return pathToAstMap.get(path).getFeature(); - } - return null; - } - - private void parseGherkinSource(final String path) { - if (!pathToReadEventMap.containsKey(path)) { - return; - } - final Parser parser = new Parser<>(new AstBuilder()); - final TokenMatcher matcher = new TokenMatcher(); - try { - final GherkinDocument gherkinDocument = parser.parse(pathToReadEventMap.get(path).source, matcher); - pathToAstMap.put(path, gherkinDocument); - final Map nodeMap = new HashMap<>(); - final AstNode currentParent = new AstNode(gherkinDocument.getFeature(), null); - for (ScenarioDefinition child : gherkinDocument.getFeature().getChildren()) { - processScenarioDefinition(nodeMap, child, currentParent); - } - pathToNodeMap.put(path, nodeMap); - } catch (ParserException e) { - LOGGER.trace(e.getMessage(), e); - } - } - - private void processScenarioDefinition( - final Map nodeMap, final ScenarioDefinition child, final AstNode currentParent - ) { - final AstNode childNode = new AstNode(child, currentParent); - nodeMap.put(child.getLocation().getLine(), childNode); - - child.getSteps().forEach( - step -> nodeMap.put(step.getLocation().getLine(), new AstNode(step, childNode)) - ); - - if (child instanceof ScenarioOutline) { - processScenarioOutlineExamples(nodeMap, (ScenarioOutline) child, childNode); - } - } - - private void processScenarioOutlineExamples( - final Map nodeMap, final ScenarioOutline scenarioOutline, final AstNode childNode - ) { - scenarioOutline.getExamples().forEach(examples -> { - final AstNode examplesNode = new AstNode(examples, childNode); - final TableRow headerRow = examples.getTableHeader(); - final AstNode headerNode = new AstNode(headerRow, examplesNode); - nodeMap.put(headerRow.getLocation().getLine(), headerNode); - IntStream.range(0, examples.getTableBody().size()).forEach(i -> { - final TableRow examplesRow = examples.getTableBody().get(i); - final Node rowNode = new CucumberSourceUtils.ExamplesRowWrapperNode(examplesRow, i); - final AstNode expandedScenarioNode = new AstNode(rowNode, examplesNode); - nodeMap.put(examplesRow.getLocation().getLine(), expandedScenarioNode); - }); - }); - } - - private AstNode getAstNode(final String path, final int line) { - if (!pathToNodeMap.containsKey(path)) { - parseGherkinSource(path); - } - if (pathToNodeMap.containsKey(path)) { - return pathToNodeMap.get(path).get(line); - } - return null; - } - - public ScenarioDefinition getScenarioDefinition(final String path, final int line) { - return getScenarioDefinition(getAstNode(path, line)); - } - - private ScenarioDefinition getScenarioDefinition(final AstNode astNode) { - return astNode.getNode() instanceof ScenarioDefinition - ? (ScenarioDefinition) astNode.getNode() - : (ScenarioDefinition) astNode.getParent().getParent().getNode(); - } - - public String getKeywordFromSource(final String uri, final int stepLine) { - final Feature feature = getFeature(uri); - if (feature != null) { - final TestSourceRead event = getTestSourceReadEvent(uri); - final String trimmedSourceLine = event.source.split("\n")[stepLine - 1].trim(); - final GherkinDialect dialect = new GherkinDialectProvider(feature.getLanguage()).getDefaultDialect(); - for (String keyword : dialect.getStepKeywords()) { - if (trimmedSourceLine.startsWith(keyword)) { - return keyword; - } - } - } - return ""; - } - - private TestSourceRead getTestSourceReadEvent(final String uri) { - if (pathToReadEventMap.containsKey(uri)) { - return pathToReadEventMap.get(uri); - } - return null; - } - - /** - * Representation of Examples row. - */ - private static class ExamplesRowWrapperNode extends Node { - private final int bodyRowIndex; - - ExamplesRowWrapperNode(final Node examplesRow, final int bodyRowIndex) { - super(examplesRow.getLocation()); - this.bodyRowIndex = bodyRowIndex; - } - - public int getBodyRowIndex() { - return bodyRowIndex; - } - } - - /** - * Representation of leaf node. - */ - private static class AstNode { - private final Node node; - private final AstNode parent; - - AstNode(final Node node, final AstNode parent) { - this.node = node; - this.parent = parent; - } - - public Node getNode() { - return node; - } - - public AstNode getParent() { - return parent; - } - } -} diff --git a/allure-cucumber2-jvm/src/test/java/io/qameta/allure/cucumber2jvm/AllureCucumber2JvmTest.java b/allure-cucumber2-jvm/src/test/java/io/qameta/allure/cucumber2jvm/AllureCucumber2JvmTest.java deleted file mode 100644 index 10498f014..000000000 --- a/allure-cucumber2-jvm/src/test/java/io/qameta/allure/cucumber2jvm/AllureCucumber2JvmTest.java +++ /dev/null @@ -1,566 +0,0 @@ -/* - * Copyright 2019 Qameta Software OÜ - * - * 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.cucumber2jvm; - -import cucumber.runtime.ClassFinder; -import cucumber.runtime.Runtime; -import cucumber.runtime.RuntimeOptions; -import cucumber.runtime.io.MultiLoader; -import cucumber.runtime.io.ResourceLoader; -import cucumber.runtime.io.ResourceLoaderClassFinder; -import cucumber.runtime.model.CucumberFeature; -import gherkin.AstBuilder; -import gherkin.Parser; -import gherkin.TokenMatcher; -import gherkin.ast.GherkinDocument; -import io.github.glytching.junit.extension.system.SystemProperty; -import io.github.glytching.junit.extension.system.SystemPropertyExtension; -import io.qameta.allure.AllureLifecycle; -import io.qameta.allure.Issue; -import io.qameta.allure.model.Attachment; -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; -import io.qameta.allure.model.TestResult; -import io.qameta.allure.test.AllureFeatures; -import io.qameta.allure.test.AllureResults; -import io.qameta.allure.test.AllureResultsWriterStub; -import org.apache.commons.io.IOUtils; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import static java.lang.Thread.currentThread; -import static java.util.Objects.nonNull; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.tuple; - -/** - * @author charlie (Dmitry Baev). - */ -@SuppressWarnings("unchecked") -class AllureCucumber2JvmTest { - - @AllureFeatures.Base - @Test - void shouldSetName() { - final AllureResults results = runFeature("features/simple.feature"); - - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getName) - .containsExactlyInAnyOrder("Add a to b"); - - } - - @AllureFeatures.PassedTests - @Test - void shouldSetStatus() { - final AllureResults results = runFeature("features/simple.feature"); - - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.PASSED); - } - - @AllureFeatures.FailedTests - @Test - void shouldSetFailedStatus() { - final AllureResults results = runFeature("features/failed.feature"); - - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.FAILED); - } - - @AllureFeatures.FailedTests - @Test - void shouldSetStatusDetails() { - final AllureResults results = runFeature("features/failed.feature"); - - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getStatusDetails) - .extracting(StatusDetails::getMessage) - .containsExactlyInAnyOrder("expected: <15> but was: <123>"); - } - - @AllureFeatures.BrokenTests - @Test - void shouldSetBrokenStatus() { - final AllureResults results = runFeature("features/broken.feature"); - - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.BROKEN); - } - - @AllureFeatures.Stages - @Test - void shouldSetStage() { - final AllureResults results = runFeature("features/simple.feature"); - - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getStage) - .containsExactlyInAnyOrder(Stage.FINISHED); - } - - @AllureFeatures.Timeline - @Test - void shouldSetStart() { - final long before = Instant.now().toEpochMilli(); - final AllureResults results = runFeature("features/simple.feature"); - final long after = Instant.now().toEpochMilli(); - - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getStart) - .allMatch(v -> v >= before && v <= after); - } - - @AllureFeatures.Timings - @Test - void shouldSetStop() { - final long before = Instant.now().toEpochMilli(); - final AllureResults results = runFeature("features/simple.feature"); - final long after = Instant.now().toEpochMilli(); - - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getStop) - .allMatch(v -> v >= before && v <= after); - } - - @AllureFeatures.FullName - @Test - void shouldSetFullName() { - final AllureResults results = runFeature("features/simple.feature"); - - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getFullName) - .containsExactlyInAnyOrder("Simple feature: Add a to b"); - } - - @AllureFeatures.Descriptions - @Test - void shouldSetDescription() { - final AllureResults results = runFeature("features/description.feature"); - - final String expected = "This is description for current feature.\n" - + "It should appear on each scenario in report"; - - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getDescription) - .containsExactlyInAnyOrder( - expected, - expected - ); - } - - @AllureFeatures.Attachments - @Test - void shouldAddDataTableAttachment() { - final AllureResults results = runFeature("features/datatable.feature"); - - final List attachments = results.getTestResults().stream() - .map(TestResult::getSteps) - .flatMap(Collection::stream) - .map(StepResult::getAttachments) - .flatMap(Collection::stream) - .collect(Collectors.toList()); - - assertThat(attachments) - .extracting(Attachment::getName, Attachment::getType) - .containsExactlyInAnyOrder( - tuple("Data table", "text/tab-separated-values") - ); - - final Attachment dataTableAttachment = attachments.iterator().next(); - final Map attachmentFiles = results.getAttachments(); - assertThat(attachmentFiles) - .containsKeys(dataTableAttachment.getSource()); - - final byte[] bytes = attachmentFiles.get(dataTableAttachment.getSource()); - final String attachmentContent = new String(bytes, StandardCharsets.UTF_8); - - assertThat(attachmentContent) - .isEqualTo("name\tlogin\temail\n" + - "Viktor\tclicman\tclicman@ya.ru\n" + - "Viktor2\tclicman2\tclicman2@ya.ru\n" - ); - - } - - @AllureFeatures.Attachments - @Test - void shouldAddAttachments() { - final AllureResults results = runFeature("features/attachments.feature"); - - final List attachments = results.getTestResults().stream() - .map(TestResult::getSteps) - .flatMap(Collection::stream) - .map(StepResult::getAttachments) - .flatMap(Collection::stream) - .collect(Collectors.toList()); - - assertThat(attachments) - .extracting(Attachment::getName, Attachment::getType) - .containsExactlyInAnyOrder( - tuple("Text output", "text/plain"), - tuple("Screenshot", null) - ); - - final List attachmentContents = results.getAttachments().values().stream() - .map(bytes -> new String(bytes, StandardCharsets.UTF_8)) - .collect(Collectors.toList()); - - assertThat(attachmentContents) - .containsExactlyInAnyOrder("text attachment", "image attachment"); - } - - @AllureFeatures.Steps - @Test - void shouldAddBackgroundSteps() { - final AllureResults results = runFeature("features/background.feature"); - - final List testResults = results.getTestResults(); - - assertThat(testResults) - .hasSize(1) - .flatExtracting(TestResult::getSteps) - .extracting(StepResult::getName) - .containsExactly( - "Given cat is sad", - "And cat is murmur", - "When Pet the cat", - "Then Cat is happy" - ); - } - - @AllureFeatures.Parameters - @Test - void shouldAddParametersFromExamples() { - final AllureResults results = runFeature("features/examples.feature"); - - final List testResults = results.getTestResults(); - - assertThat(testResults) - .hasSize(2); - - assertThat(testResults.get(0).getParameters()) - .hasSize(3); - - assertThat(testResults.get(1).getParameters()) - .hasSize(3); - - assertThat(testResults) - .flatExtracting(TestResult::getParameters) - .extracting(Parameter::getName, Parameter::getValue) - .containsExactlyInAnyOrder( - tuple("a", "1"), tuple("b", "3"), tuple("result", "4"), - tuple("a", "2"), tuple("b", "4"), tuple("result", "6") - ); - } - - @AllureFeatures.Parameters - @Test - void shouldHandleMultipleExampleTables() { - final AllureResults results = runFeature("features/multipleExamples.feature"); - - final List testResults = results.getTestResults(); - - assertThat(testResults) - .hasSize(2); - } - - @AllureFeatures.MarkerAnnotations - @Test - void shouldAddTags() { - final AllureResults results = runFeature("features/tags.feature"); - - final List testResults = results.getTestResults(); - - assertThat(testResults) - .flatExtracting(TestResult::getLabels) - .extracting(Label::getName, Label::getValue) - .contains( - tuple("tag", "FeatureTag"), - tuple("tag", "good") - ); - } - - @AllureFeatures.Links - @ExtendWith(SystemPropertyExtension.class) - @SystemProperty(name = "allure.link.issue.pattern", value = "https://example.org/issue/{}") - @SystemProperty(name = "allure.link.tms.pattern", value = "https://example.org/tms/{}") - @Test - void shouldAddLinks() { - final AllureResults results = runFeature("features/tags.feature"); - - final List testResults = results.getTestResults(); - - assertThat(testResults) - .flatExtracting(TestResult::getLinks) - .extracting(Link::getName, Link::getType, Link::getUrl) - .contains( - tuple("OAT-4444", "tms", "https://example.org/tms/OAT-4444"), - tuple("BUG-22400", "issue", "https://example.org/issue/BUG-22400") - ); - } - - @AllureFeatures.MarkerAnnotations - @Test - void shouldAddBddLabels() { - final AllureResults results = runFeature("features/tags.feature"); - - final List testResults = results.getTestResults(); - - assertThat(testResults) - .flatExtracting(TestResult::getLabels) - .extracting(Label::getName, Label::getValue) - .contains( - tuple("feature", "Test Simple Scenarios"), - tuple("story", "Add a to b") - ); - } - - @AllureFeatures.Timeline - @Test - void shouldThreadHostLabels() { - final AllureResults results = runFeature("features/tags.feature"); - - final List testResults = results.getTestResults(); - - assertThat(testResults) - .flatExtracting(TestResult::getLabels) - .extracting(Label::getName) - .contains("host", "thread"); - } - - @AllureFeatures.MarkerAnnotations - @Test - void shouldCommonLabels() { - final AllureResults results = runFeature("features/tags.feature"); - - final List testResults = results.getTestResults(); - - assertThat(testResults) - .flatExtracting(TestResult::getLabels) - .extracting(Label::getName, Label::getValue) - .contains( - tuple("package", "Test Simple Scenarios"), - tuple("suite", "Test Simple Scenarios"), - tuple("testClass", "Add a to b") - ); - } - - @AllureFeatures.NotImplementedTests - @Test - void shouldProcessNotImplementedScenario() { - final AllureResults results = runFeature("features/undefined.feature"); - - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.PASSED); - } - - @AllureFeatures.Base - @Test - void shouldSupportDryRun() { - final AllureResults results = runFeature("features/simple.feature", "--dry-run"); - - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getName, TestResult::getStatus) - .containsExactlyInAnyOrder( - tuple("Add a to b", Status.SKIPPED) - ); - } - - @AllureFeatures.Base - @Issue("173") - @Issue("164") - @Test - void shouldUseUuid() { - final AllureResults results = runFeature("features/simple.feature"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getUuid) - .allMatch(uuid -> nonNull(uuid) && uuid.matches("[\\-a-z0-9]+"), "UUID"); - } - - private AllureResults runFeature(final String featureResource, - final String... moreOptions) { - final AllureResultsWriterStub writer = new AllureResultsWriterStub(); - final AllureLifecycle lifecycle = new AllureLifecycle(writer); - final AllureCucumber2Jvm cucumber2Jvm = new AllureCucumber2Jvm(lifecycle); - final ClassLoader classLoader = currentThread().getContextClassLoader(); - final ResourceLoader resourceLoader = new MultiLoader(classLoader); - final ClassFinder classFinder = new ResourceLoaderClassFinder(resourceLoader, classLoader); - final List opts = new ArrayList<>(Arrays.asList( - "--glue", "io.qameta.allure.cucumber2jvm.samples", - "--plugin", "null" - )); - opts.addAll(Arrays.asList(moreOptions)); - final RuntimeOptions options = new RuntimeOptions(opts); - final Runtime runtime = new Runtime(resourceLoader, classFinder, classLoader, options); - - options.addPlugin(cucumber2Jvm); - - final String gherkin = readResource(featureResource); - Parser parser = new Parser<>(new AstBuilder()); - TokenMatcher matcher = new TokenMatcher(); - GherkinDocument gherkinDocument = parser.parse(gherkin, matcher); - CucumberFeature feature = new CucumberFeature(gherkinDocument, featureResource, gherkin); - - feature.sendTestSourceRead(runtime.getEventBus()); - runtime.runFeature(feature); - return writer; - } - - @AllureFeatures.Fixtures - @Test - void shouldSetStatusFailedOnBadAfter() { - final AllureResults results = runFeature("features/hooks.feature", "-t", "@bp_sf_af"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.FAILED); - } - - - @AllureFeatures.Fixtures - @Test - void shouldSetStatusSkippedOnBadBefore() { - final AllureResults results = runFeature("features/hooks.feature", "-t", "@bf_ap"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.SKIPPED); - } - - @AllureFeatures.Fixtures - @Test - void shouldSetStatusSkippedOnBadBeforeAndBadAfter() { - final AllureResults results = runFeature("features/hooks.feature", "-t", "@bf_af"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.SKIPPED); - } - - @AllureFeatures.Fixtures - @Test - void shouldSetStatusBrokenOnBadAfter() { - final AllureResults results = runFeature("features/hooks.feature", "-t", "@bp_af"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.BROKEN); - } - - @AllureFeatures.Fixtures - @Test - void shouldSetStatusFailedOnBadSteps() { - final AllureResults results = runFeature("features/hooks.feature", "-t", "@bp_sf_ap"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.FAILED); - } - - @AllureFeatures.NotImplementedTests - @Test - void shouldSetStatusBrokenOnUndefinedStepsAndBadAfter() { - final AllureResults results = runFeature("features/hooks.feature", "-t", "@bp_su_af"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.BROKEN); - } - - - @AllureFeatures.NotImplementedTests - @Test - void shouldSetStatusPassedOnUndefinedSteps() { - final AllureResults results = runFeature("features/hooks.feature", "-t", "@bp_su_ap"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.PASSED); - } - - @AllureFeatures.NotImplementedTests - @Test - void shouldSetStatusSkippedOnUndefinedAndFailedSteps() { - final AllureResults results = runFeature("features/hooks.feature", "-t", "@bp_suf_ap"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.SKIPPED); - } - - - @AllureFeatures.NotImplementedTests - @Test - void shouldSetStatusPassedOnPassedAndUndefinedSteps() { - final AllureResults results = runFeature("features/hooks.feature", "-t", "@bp_spu_ap"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.PASSED); - } - - @AllureFeatures.NotImplementedTests - @Test - void shouldSetStatusFailedOnFailedAndUndefinedSteps() { - final AllureResults results = runFeature("features/hooks.feature", "-t", "@bp_sfu_ap"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.FAILED); - } - - - private String readResource(final String resourceName) { - try (InputStream is = currentThread().getContextClassLoader().getResourceAsStream(resourceName)) { - return IOUtils.toString(is, StandardCharsets.UTF_8); - } catch (IOException e) { - throw new RuntimeException("Feature file not found " + resourceName); - } - } -} diff --git a/allure-cucumber2-jvm/src/test/resources/features/undefined.feature b/allure-cucumber2-jvm/src/test/resources/features/undefined.feature deleted file mode 100644 index caa0b507b..000000000 --- a/allure-cucumber2-jvm/src/test/resources/features/undefined.feature +++ /dev/null @@ -1,4 +0,0 @@ -Feature: Simple feature - - Scenario: Step is not implemented - Given hello my friend diff --git a/allure-cucumber3-jvm/src/main/java/io/qameta/allure/cucumber3jvm/AllureCucumber3Jvm.java b/allure-cucumber3-jvm/src/main/java/io/qameta/allure/cucumber3jvm/AllureCucumber3Jvm.java deleted file mode 100644 index 94ce9b6fc..000000000 --- a/allure-cucumber3-jvm/src/main/java/io/qameta/allure/cucumber3jvm/AllureCucumber3Jvm.java +++ /dev/null @@ -1,411 +0,0 @@ -/* - * Copyright 2019 Qameta Software OÜ - * - * 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.cucumber3jvm; - -import cucumber.api.HookTestStep; -import cucumber.api.HookType; -import cucumber.api.PendingException; -import cucumber.api.PickleStepTestStep; -import cucumber.api.Result; -import cucumber.api.TestCase; -import cucumber.api.event.EmbedEvent; -import cucumber.api.event.EventHandler; -import cucumber.api.event.EventPublisher; -import cucumber.api.event.TestCaseFinished; -import cucumber.api.event.TestCaseStarted; -import cucumber.api.event.TestSourceRead; -import cucumber.api.event.TestStepFinished; -import cucumber.api.event.TestStepStarted; -import cucumber.api.event.WriteEvent; -import cucumber.api.formatter.Formatter; -import gherkin.ast.Examples; -import gherkin.ast.Feature; -import gherkin.ast.ScenarioDefinition; -import gherkin.ast.ScenarioOutline; -import gherkin.ast.TableRow; -import gherkin.pickles.PickleCell; -import gherkin.pickles.PickleRow; -import gherkin.pickles.PickleTable; -import gherkin.pickles.PickleTag; -import io.qameta.allure.Allure; -import io.qameta.allure.AllureLifecycle; -import io.qameta.allure.model.FixtureResult; -import io.qameta.allure.model.Parameter; -import io.qameta.allure.model.Status; -import io.qameta.allure.model.StatusDetails; -import io.qameta.allure.model.StepResult; -import io.qameta.allure.model.TestResult; -import io.qameta.allure.model.TestResultContainer; - -import java.io.ByteArrayInputStream; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.Deque; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.UUID; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import static io.qameta.allure.util.ResultsUtils.createParameter; -import static io.qameta.allure.util.ResultsUtils.getStatus; -import static io.qameta.allure.util.ResultsUtils.getStatusDetails; -import static io.qameta.allure.util.ResultsUtils.md5; - -/** - * Allure plugin for Cucumber JVM 3.0. - */ -@SuppressWarnings({ - "PMD.ExcessiveImports", - "ClassFanOutComplexity", "ClassDataAbstractionCoupling" -}) -public class AllureCucumber3Jvm implements Formatter { - - private final AllureLifecycle lifecycle; - - private final Map scenarioUuids = new HashMap<>(); - - private final CucumberSourceUtils cucumberSourceUtils = new CucumberSourceUtils(); - private Feature currentFeature; - private String currentFeatureFile; - private TestCase currentTestCase; - private String currentContainer; - private boolean forbidTestCaseStatusChange; - - private final EventHandler featureStartedHandler = this::handleFeatureStartedHandler; - private final EventHandler caseStartedHandler = this::handleTestCaseStarted; - private final EventHandler caseFinishedHandler = this::handleTestCaseFinished; - private final EventHandler stepStartedHandler = this::handleTestStepStarted; - private final EventHandler stepFinishedHandler = this::handleTestStepFinished; - private final EventHandler writeEventHandler = this::handleWriteEvent; - private final EventHandler embedEventHandler = this::handleEmbedEvent; - - private static final String TXT_EXTENSION = ".txt"; - private static final String TEXT_PLAIN = "text/plain"; - - @SuppressWarnings("unused") - public AllureCucumber3Jvm() { - this(Allure.getLifecycle()); - } - - public AllureCucumber3Jvm(final AllureLifecycle lifecycle) { - this.lifecycle = lifecycle; - } - - @Override - public void setEventPublisher(final EventPublisher publisher) { - publisher.registerHandlerFor(TestSourceRead.class, featureStartedHandler); - - publisher.registerHandlerFor(TestCaseStarted.class, caseStartedHandler); - publisher.registerHandlerFor(TestCaseFinished.class, caseFinishedHandler); - - publisher.registerHandlerFor(TestStepStarted.class, stepStartedHandler); - publisher.registerHandlerFor(TestStepFinished.class, stepFinishedHandler); - - publisher.registerHandlerFor(WriteEvent.class, writeEventHandler); - publisher.registerHandlerFor(EmbedEvent.class, embedEventHandler); - } - - /* - Event Handlers - */ - - private void handleFeatureStartedHandler(final TestSourceRead event) { - cucumberSourceUtils.addTestSourceReadEvent(event.uri, event); - } - - private void handleTestCaseStarted(final TestCaseStarted event) { - currentTestCase = event.testCase; - currentFeatureFile = currentTestCase.getUri(); - currentFeature = cucumberSourceUtils.getFeature(currentFeatureFile); - currentContainer = UUID.randomUUID().toString(); - forbidTestCaseStatusChange = false; - - - final Deque tags = new LinkedList<>(currentTestCase.getTags()); - - final LabelBuilder labelBuilder = new LabelBuilder(currentFeature, currentTestCase, tags); - - final String name = currentTestCase.getName(); - final String featureName = currentFeature.getName(); - - final TestResult result = new TestResult() - .setUuid(getTestCaseUuid(currentTestCase)) - .setHistoryId(getHistoryId(currentTestCase)) - .setFullName(featureName + ": " + name) - .setName(name) - .setLabels(labelBuilder.getScenarioLabels()) - .setLinks(labelBuilder.getScenarioLinks()); - - final ScenarioDefinition scenarioDefinition = - cucumberSourceUtils.getScenarioDefinition(currentFeatureFile, currentTestCase.getLine()); - if (scenarioDefinition instanceof ScenarioOutline) { - result.setParameters( - getExamplesAsParameters((ScenarioOutline) scenarioDefinition) - ); - } - - if (currentFeature.getDescription() != null && !currentFeature.getDescription().isEmpty()) { - result.setDescription(currentFeature.getDescription()); - } - - final TestResultContainer resultContainer = new TestResultContainer() - .setName(String.format("%s: %s", scenarioDefinition.getKeyword(), scenarioDefinition.getName())) - .setUuid(getTestContainerUuid()) - .setChildren(Collections.singletonList(getTestCaseUuid(currentTestCase))); - - lifecycle.scheduleTestCase(result); - lifecycle.startTestContainer(getTestContainerUuid(), resultContainer); - lifecycle.startTestCase(getTestCaseUuid(currentTestCase)); - } - - private void handleTestCaseFinished(final TestCaseFinished event) { - - final String uuid = getTestCaseUuid(event.testCase); - final Optional details = getStatusDetails(event.result.getError()); - details.ifPresent(statusDetails -> lifecycle.updateTestCase( - uuid, - testResult -> testResult.setStatusDetails(statusDetails) - )); - lifecycle.stopTestCase(uuid); - lifecycle.stopTestContainer(getTestContainerUuid()); - lifecycle.writeTestCase(uuid); - lifecycle.writeTestContainer(getTestContainerUuid()); - } - - private void handleTestStepStarted(final TestStepStarted event) { - if (event.testStep instanceof PickleStepTestStep) { - final PickleStepTestStep pickleStep = (PickleStepTestStep) event.testStep; - final String stepKeyword = Optional.ofNullable( - cucumberSourceUtils.getKeywordFromSource(currentFeatureFile, pickleStep.getStepLine()) - ).orElse("UNDEFINED"); - - final StepResult stepResult = new StepResult() - .setName(String.format("%s %s", stepKeyword, pickleStep.getPickleStep().getText())) - .setStart(System.currentTimeMillis()); - - lifecycle.startStep(getTestCaseUuid(currentTestCase), getStepUuid(pickleStep), stepResult); - - pickleStep.getStepArgument().stream() - .filter(PickleTable.class::isInstance) - .findFirst() - .ifPresent(table -> createDataTableAttachment((PickleTable) table)); - } else if (event.testStep instanceof HookTestStep) { - initHook((HookTestStep) event.testStep); - } - } - - private void initHook(final HookTestStep hook) { - - final FixtureResult hookResult = new FixtureResult() - .setName(hook.getCodeLocation()) - .setStart(System.currentTimeMillis()); - - if (hook.getHookType() == HookType.Before) { - lifecycle.startPrepareFixture(getTestContainerUuid(), getHookStepUuid(hook), hookResult); - } else { - lifecycle.startTearDownFixture(getTestContainerUuid(), getHookStepUuid(hook), hookResult); - } - - } - - private void handleTestStepFinished(final TestStepFinished event) { - if (event.testStep instanceof HookTestStep) { - handleHookStep(event); - } else { - handlePickleStep(event); - } - } - - private void handleWriteEvent(final WriteEvent event) { - lifecycle.addAttachment( - "Text output", - TEXT_PLAIN, - TXT_EXTENSION, - Objects.toString(event.text).getBytes(StandardCharsets.UTF_8) - ); - } - - private void handleEmbedEvent(final EmbedEvent event) { - lifecycle.addAttachment("Screenshot", null, null, new ByteArrayInputStream(event.data)); - } - - /* - Utility Methods - */ - - private String getTestContainerUuid() { - return currentContainer; - } - - private String getTestCaseUuid(final TestCase testCase) { - return scenarioUuids.computeIfAbsent(getHistoryId(testCase), it -> UUID.randomUUID().toString()); - } - - private String getStepUuid(final PickleStepTestStep step) { - return currentFeature.getName() + getTestCaseUuid(currentTestCase) - + step.getPickleStep().getText() + step.getStepLine(); - } - - private String getHookStepUuid(final HookTestStep step) { - return currentFeature.getName() + getTestCaseUuid(currentTestCase) - + step.getHookType().toString() + step.getCodeLocation(); - } - - private String getHistoryId(final TestCase testCase) { - final String testCaseLocation = testCase.getUri() + ":" + testCase.getLine(); - return md5(testCaseLocation); - } - - private Status translateTestCaseStatus(final Result testCaseResult) { - switch (testCaseResult.getStatus()) { - case FAILED: - return getStatus(testCaseResult.getError()) - .orElse(Status.FAILED); - case PASSED: - return Status.PASSED; - case SKIPPED: - case PENDING: - return Status.SKIPPED; - case AMBIGUOUS: - case UNDEFINED: - default: - return null; - } - } - - private List getExamplesAsParameters(final ScenarioOutline scenarioOutline) { - final Optional examplesBlock = - scenarioOutline.getExamples().stream() - .filter(example -> example.getTableBody().stream() - .anyMatch(row -> row.getLocation().getLine() == currentTestCase.getLine()) - ).findFirst(); - - if (examplesBlock.isPresent()) { - final TableRow row = examplesBlock.get().getTableBody().stream() - .filter(example -> example.getLocation().getLine() == currentTestCase.getLine()) - .findFirst().get(); - return IntStream.range(0, examplesBlock.get().getTableHeader().getCells().size()).mapToObj(index -> { - final String name = examplesBlock.get().getTableHeader().getCells().get(index).getValue(); - final String value = row.getCells().get(index).getValue(); - return createParameter(name, value); - }).collect(Collectors.toList()); - } else { - return Collections.emptyList(); - } - } - - private void createDataTableAttachment(final PickleTable pickleTable) { - final List rows = pickleTable.getRows(); - - final StringBuilder dataTableCsv = new StringBuilder(); - if (!rows.isEmpty()) { - rows.forEach(dataTableRow -> { - dataTableCsv.append( - dataTableRow.getCells().stream() - .map(PickleCell::getValue) - .collect(Collectors.joining("\t")) - ); - dataTableCsv.append('\n'); - }); - - final String attachmentSource = lifecycle - .prepareAttachment("Data table", "text/tab-separated-values", "csv"); - lifecycle.writeAttachment(attachmentSource, - new ByteArrayInputStream(dataTableCsv.toString().getBytes(Charset.forName("UTF-8")))); - } - } - - private void handleHookStep(final TestStepFinished event) { - final HookTestStep hookStep = (HookTestStep) event.testStep; - final String uuid = getHookStepUuid(hookStep); - final FixtureResult fixtureResult = new FixtureResult().setStatus(translateTestCaseStatus(event.result)); - - if (!Status.PASSED.equals(fixtureResult.getStatus())) { - final TestResult testResult = new TestResult().setStatus(translateTestCaseStatus(event.result)); - final StatusDetails statusDetails = getStatusDetails(event.result.getError()).get(); - - statusDetails.setMessage(hookStep.getHookType() - .name() + " is failed: " + event.result.getError().getLocalizedMessage()); - - if (hookStep.getHookType() == HookType.Before) { - final TagParser tagParser = new TagParser(currentFeature, currentTestCase); - statusDetails - .setFlaky(tagParser.isFlaky()) - .setMuted(tagParser.isMuted()) - .setKnown(tagParser.isKnown()); - testResult.setStatus(Status.SKIPPED); - updateTestCaseStatus(testResult.getStatus()); - forbidTestCaseStatusChange = true; - } else { - testResult.setStatus(Status.BROKEN); - updateTestCaseStatus(testResult.getStatus()); - } - fixtureResult.setStatusDetails(statusDetails); - } - - lifecycle.updateFixture(uuid, result -> result.setStatus(fixtureResult.getStatus()) - .setStatusDetails(fixtureResult.getStatusDetails())); - lifecycle.stopFixture(uuid); - } - - private void handlePickleStep(final TestStepFinished event) { - - final Status stepStatus = translateTestCaseStatus(event.result); - final StatusDetails statusDetails; - if (event.result.getStatus() == Result.Type.UNDEFINED) { - updateTestCaseStatus(Status.PASSED); - - statusDetails = - getStatusDetails(new PendingException("TODO: implement me")) - .orElse(new StatusDetails()); - lifecycle.updateTestCase(getTestCaseUuid(currentTestCase), scenarioResult -> - scenarioResult - .setStatusDetails(statusDetails)); - } else { - statusDetails = - getStatusDetails(event.result.getError()) - .orElse(new StatusDetails()); - updateTestCaseStatus(stepStatus); - } - - if (!Status.PASSED.equals(stepStatus) && stepStatus != null) { - forbidTestCaseStatusChange = true; - } - - final TagParser tagParser = new TagParser(currentFeature, currentTestCase); - statusDetails - .setFlaky(tagParser.isFlaky()) - .setMuted(tagParser.isMuted()) - .setKnown(tagParser.isKnown()); - - lifecycle.updateStep(getStepUuid((PickleStepTestStep) event.testStep), - stepResult -> stepResult.setStatus(stepStatus).setStatusDetails(statusDetails)); - lifecycle.stopStep(getStepUuid((PickleStepTestStep) event.testStep)); - } - - private void updateTestCaseStatus(final Status status) { - if (!forbidTestCaseStatusChange) { - lifecycle.updateTestCase(getTestCaseUuid(currentTestCase), - result -> result.setStatus(status)); - } - } -} diff --git a/allure-cucumber3-jvm/src/main/java/io/qameta/allure/cucumber3jvm/CucumberSourceUtils.java b/allure-cucumber3-jvm/src/main/java/io/qameta/allure/cucumber3jvm/CucumberSourceUtils.java deleted file mode 100644 index 2f2ed0f37..000000000 --- a/allure-cucumber3-jvm/src/main/java/io/qameta/allure/cucumber3jvm/CucumberSourceUtils.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright 2019 Qameta Software OÜ - * - * 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.cucumber3jvm; - -import cucumber.api.event.TestSourceRead; - -import gherkin.Parser; -import gherkin.AstBuilder; -import gherkin.TokenMatcher; -import gherkin.ParserException; -import gherkin.GherkinDialect; -import gherkin.GherkinDialectProvider; - -import gherkin.ast.GherkinDocument; -import gherkin.ast.Feature; -import gherkin.ast.ScenarioDefinition; -import gherkin.ast.Node; -import gherkin.ast.ScenarioOutline; -import gherkin.ast.TableRow; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.HashMap; -import java.util.Map; -import java.util.stream.IntStream; - -/** - * Parts of package-private cucumber.runtime.formatter.TestSourcesModel needed for Allure 2 adapter. - */ -final class CucumberSourceUtils { - - private static final Logger LOGGER = LoggerFactory.getLogger(CucumberSourceUtils.class); - - private final Map pathToReadEventMap = new HashMap<>(); - private final Map pathToAstMap = new HashMap<>(); - private final Map> pathToNodeMap = new HashMap<>(); - - public void addTestSourceReadEvent(final String path, final TestSourceRead event) { - pathToReadEventMap.put(path, event); - } - - public Feature getFeature(final String path) { - if (!pathToAstMap.containsKey(path)) { - parseGherkinSource(path); - } - if (pathToAstMap.containsKey(path)) { - return pathToAstMap.get(path).getFeature(); - } - return null; - } - - private void parseGherkinSource(final String path) { - if (!pathToReadEventMap.containsKey(path)) { - return; - } - final Parser parser = new Parser<>(new AstBuilder()); - final TokenMatcher matcher = new TokenMatcher(); - try { - final GherkinDocument gherkinDocument = parser.parse(pathToReadEventMap.get(path).source, matcher); - pathToAstMap.put(path, gherkinDocument); - final Map nodeMap = new HashMap<>(); - final AstNode currentParent = new AstNode(gherkinDocument.getFeature(), null); - for (ScenarioDefinition child : gherkinDocument.getFeature().getChildren()) { - processScenarioDefinition(nodeMap, child, currentParent); - } - pathToNodeMap.put(path, nodeMap); - } catch (ParserException e) { - LOGGER.trace(e.getMessage(), e); - } - } - - private void processScenarioDefinition( - final Map nodeMap, final ScenarioDefinition child, final AstNode currentParent - ) { - final AstNode childNode = new AstNode(child, currentParent); - nodeMap.put(child.getLocation().getLine(), childNode); - - child.getSteps().forEach( - step -> nodeMap.put(step.getLocation().getLine(), new AstNode(step, childNode)) - ); - - if (child instanceof ScenarioOutline) { - processScenarioOutlineExamples(nodeMap, (ScenarioOutline) child, childNode); - } - } - - private void processScenarioOutlineExamples( - final Map nodeMap, final ScenarioOutline scenarioOutline, final AstNode childNode - ) { - scenarioOutline.getExamples().forEach(examples -> { - final AstNode examplesNode = new AstNode(examples, childNode); - final TableRow headerRow = examples.getTableHeader(); - final AstNode headerNode = new AstNode(headerRow, examplesNode); - nodeMap.put(headerRow.getLocation().getLine(), headerNode); - IntStream.range(0, examples.getTableBody().size()).forEach(i -> { - final TableRow examplesRow = examples.getTableBody().get(i); - final Node rowNode = new CucumberSourceUtils.ExamplesRowWrapperNode(examplesRow, i); - final AstNode expandedScenarioNode = new AstNode(rowNode, examplesNode); - nodeMap.put(examplesRow.getLocation().getLine(), expandedScenarioNode); - }); - }); - } - - private AstNode getAstNode(final String path, final int line) { - if (!pathToNodeMap.containsKey(path)) { - parseGherkinSource(path); - } - if (pathToNodeMap.containsKey(path)) { - return pathToNodeMap.get(path).get(line); - } - return null; - } - - public ScenarioDefinition getScenarioDefinition(final String path, final int line) { - return getScenarioDefinition(getAstNode(path, line)); - } - - private ScenarioDefinition getScenarioDefinition(final AstNode astNode) { - return astNode.getNode() instanceof ScenarioDefinition - ? (ScenarioDefinition) astNode.getNode() - : (ScenarioDefinition) astNode.getParent().getParent().getNode(); - } - - public String getKeywordFromSource(final String uri, final int stepLine) { - final Feature feature = getFeature(uri); - if (feature != null) { - final TestSourceRead event = getTestSourceReadEvent(uri); - final String trimmedSourceLine = event.source.split("\n")[stepLine - 1].trim(); - final GherkinDialect dialect = new GherkinDialectProvider(feature.getLanguage()).getDefaultDialect(); - for (String keyword : dialect.getStepKeywords()) { - if (trimmedSourceLine.startsWith(keyword)) { - return keyword; - } - } - } - return ""; - } - - private TestSourceRead getTestSourceReadEvent(final String uri) { - if (pathToReadEventMap.containsKey(uri)) { - return pathToReadEventMap.get(uri); - } - return null; - } - - /** - * Representation of Examples row. - */ - private static class ExamplesRowWrapperNode extends Node { - private final int bodyRowIndex; - - ExamplesRowWrapperNode(final Node examplesRow, final int bodyRowIndex) { - super(examplesRow.getLocation()); - this.bodyRowIndex = bodyRowIndex; - } - - public int getBodyRowIndex() { - return bodyRowIndex; - } - } - - /** - * Representation of leaf node. - */ - private static class AstNode { - private final Node node; - private final AstNode parent; - - AstNode(final Node node, final AstNode parent) { - this.node = node; - this.parent = parent; - } - - public Node getNode() { - return node; - } - - public AstNode getParent() { - return parent; - } - } -} diff --git a/allure-cucumber3-jvm/src/test/java/io/qameta/allure/cucumber3jvm/AllureCucumber3JvmTest.java b/allure-cucumber3-jvm/src/test/java/io/qameta/allure/cucumber3jvm/AllureCucumber3JvmTest.java deleted file mode 100644 index d6aaf4e46..000000000 --- a/allure-cucumber3-jvm/src/test/java/io/qameta/allure/cucumber3jvm/AllureCucumber3JvmTest.java +++ /dev/null @@ -1,578 +0,0 @@ -/* - * Copyright 2019 Qameta Software OÜ - * - * 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.cucumber3jvm; - -import cucumber.runtime.ClassFinder; -import cucumber.runtime.Runtime; -import cucumber.runtime.RuntimeOptions; -import cucumber.runtime.io.MultiLoader; -import cucumber.runtime.io.ResourceLoader; -import cucumber.runtime.io.ResourceLoaderClassFinder; -import cucumber.runtime.model.CucumberFeature; -import gherkin.AstBuilder; -import gherkin.Parser; -import gherkin.TokenMatcher; -import gherkin.ast.GherkinDocument; -import io.github.glytching.junit.extension.system.SystemProperty; -import io.github.glytching.junit.extension.system.SystemPropertyExtension; -import io.qameta.allure.AllureLifecycle; -import io.qameta.allure.Issue; -import io.qameta.allure.model.Attachment; -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; -import io.qameta.allure.model.TestResult; -import io.qameta.allure.test.AllureFeatures; -import io.qameta.allure.test.AllureResults; -import io.qameta.allure.test.AllureResultsWriterStub; -import org.apache.commons.io.IOUtils; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import static java.lang.Thread.currentThread; -import static java.util.Objects.nonNull; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.tuple; - -/** - * @author charlie (Dmitry Baev). - */ -@SuppressWarnings("unchecked") -class AllureCucumber3JvmTest { - - @AllureFeatures.Base - @Test - void shouldSetName() { - final AllureResults results = runFeature("features/simple.feature"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getName) - .containsExactlyInAnyOrder("Add a to b"); - - } - - @AllureFeatures.PassedTests - @Test - void shouldSetStatus() { - final AllureResults results = runFeature("features/simple.feature"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.PASSED); - } - - @AllureFeatures.FailedTests - @Test - void shouldSetFailedStatus() { - final AllureResults results = runFeature("features/failed.feature"); - - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.FAILED); - } - - @AllureFeatures.FailedTests - @Test - void shouldSetStatusDetails() { - final AllureResults results = runFeature("features/failed.feature"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStatusDetails) - .extracting(StatusDetails::getMessage) - .containsExactlyInAnyOrder("expected: <15> but was: <123>"); - } - - @AllureFeatures.BrokenTests - @Test - void shouldSetBrokenStatus() { - final AllureResults results = runFeature("features/broken.feature"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.BROKEN); - } - - @AllureFeatures.Stages - @Test - void shouldSetStage() { - final AllureResults results = runFeature("features/simple.feature"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStage) - .containsExactlyInAnyOrder(Stage.FINISHED); - } - - @AllureFeatures.Timings - @Test - void shouldSetStart() { - final long before = Instant.now().toEpochMilli(); - final AllureResults results = runFeature("features/simple.feature"); - final long after = Instant.now().toEpochMilli(); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStart) - .allMatch(v -> v >= before && v <= after); - } - - @AllureFeatures.Timings - @Test - void shouldSetStop() { - final long before = Instant.now().toEpochMilli(); - final AllureResults results = runFeature("features/simple.feature"); - final long after = Instant.now().toEpochMilli(); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStop) - .allMatch(v -> v >= before && v <= after); - } - - @AllureFeatures.FullName - @Test - void shouldSetFullName() { - final AllureResults results = runFeature("features/simple.feature"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getFullName) - .containsExactlyInAnyOrder("Simple feature: Add a to b"); - } - - @AllureFeatures.Descriptions - @Test - void shouldSetDescription() { - final AllureResults results = runFeature("features/description.feature"); - - final String expected = "This is description for current feature.\n" - + "It should appear on each scenario in report"; - - assertThat(results.getTestResults()) - .extracting(TestResult::getDescription) - .containsExactlyInAnyOrder( - expected, - expected - ); - } - - @AllureFeatures.Attachments - @Test - void shouldAddDataTableAttachment() { - final AllureResults results = runFeature("features/datatable.feature"); - - final List attachments = results.getTestResults().stream() - .map(TestResult::getSteps) - .flatMap(Collection::stream) - .map(StepResult::getAttachments) - .flatMap(Collection::stream) - .collect(Collectors.toList()); - - assertThat(attachments) - .extracting(Attachment::getName, Attachment::getType) - .containsExactlyInAnyOrder( - tuple("Data table", "text/tab-separated-values") - ); - - final Attachment dataTableAttachment = attachments.iterator().next(); - final Map attachmentFiles = results.getAttachments(); - assertThat(attachmentFiles) - .containsKeys(dataTableAttachment.getSource()); - - final byte[] bytes = attachmentFiles.get(dataTableAttachment.getSource()); - final String attachmentContent = new String(bytes, StandardCharsets.UTF_8); - - assertThat(attachmentContent) - .isEqualTo("name\tlogin\temail\n" + - "Viktor\tclicman\tclicman@ya.ru\n" + - "Viktor2\tclicman2\tclicman2@ya.ru\n" - ); - - } - - @AllureFeatures.Attachments - @Test - void shouldAddAttachments() { - final AllureResults results = runFeature("features/attachments.feature"); - - final List attachments = results.getTestResults().stream() - .map(TestResult::getSteps) - .flatMap(Collection::stream) - .map(StepResult::getAttachments) - .flatMap(Collection::stream) - .collect(Collectors.toList()); - - assertThat(attachments) - .extracting(Attachment::getName, Attachment::getType) - .containsExactlyInAnyOrder( - tuple("Text output", "text/plain"), - tuple("Screenshot", null) - ); - - final List attachmentContents = results.getAttachments().values().stream() - .map(bytes -> new String(bytes, StandardCharsets.UTF_8)) - .collect(Collectors.toList()); - - assertThat(attachmentContents) - .containsExactlyInAnyOrder("text attachment", "image attachment"); - } - - @AllureFeatures.Steps - @Test - void shouldAddBackgroundSteps() { - final AllureResults results = runFeature("features/background.feature"); - - assertThat(results.getTestResults()) - .hasSize(1) - .flatExtracting(TestResult::getSteps) - .extracting(StepResult::getName) - .containsExactly( - "Given cat is sad", - "And cat is murmur", - "When Pet the cat", - "Then Cat is happy" - ); - } - - @AllureFeatures.Parameters - @Test - void shouldAddParametersFromExamples() { - final AllureResults results = runFeature("features/examples.feature"); - - final List testResults = results.getTestResults(); - - assertThat(testResults) - .hasSize(2); - - assertThat(testResults.get(0).getParameters()) - .hasSize(3); - - assertThat(testResults.get(1).getParameters()) - .hasSize(3); - - assertThat(testResults) - .flatExtracting(TestResult::getParameters) - .extracting(Parameter::getName, Parameter::getValue) - .containsExactlyInAnyOrder( - tuple("a", "1"), tuple("b", "3"), tuple("result", "4"), - tuple("a", "2"), tuple("b", "4"), tuple("result", "6") - ); - - } - - @AllureFeatures.Parameters - @Test - void shouldHandleMultipleExamplesPerOutline() throws IOException { - final AllureResults results = runFeature("features/multi-examples.feature"); - - final List testResults = results.getTestResults(); - - assertThat(testResults) - .hasSize(2); - - assertThat(testResults) - .flatExtracting(TestResult::getParameters) - .extracting(Parameter::getName, Parameter::getValue) - .containsExactlyInAnyOrder( - tuple("a", "1"), tuple("b", "3"), tuple("result", "4"), - tuple("a", "2"), tuple("b", "4"), tuple("result", "6") - ); - } - - @AllureFeatures.Parameters - @Test - void shouldSupportTaggedExamplesBlocks() throws IOException { - final AllureResults results = runFeature("features/multi-examples.feature", "--tags", "@ExamplesTag2"); - - final List testResults = results.getTestResults(); - - assertThat(testResults) - .hasSize(1); - - assertThat(testResults) - .flatExtracting(TestResult::getLabels) - .extracting(Label::getName, Label::getValue) - .contains( - tuple("tag", "ExamplesTag2") - ); - - assertThat(testResults) - .flatExtracting(TestResult::getParameters) - .extracting(Parameter::getName, Parameter::getValue) - .containsExactlyInAnyOrder( - tuple("a", "2"), tuple("b", "4"), tuple("result", "6") - ); - } - - @AllureFeatures.MarkerAnnotations - @Test - void shouldAddTags() { - final AllureResults results = runFeature("features/tags.feature"); - - assertThat(results.getTestResults()) - .flatExtracting(TestResult::getLabels) - .extracting(Label::getName, Label::getValue) - .contains( - tuple("tag", "FeatureTag"), - tuple("tag", "good") - ); - } - - @AllureFeatures.Links - @ExtendWith(SystemPropertyExtension.class) - @SystemProperty(name = "allure.link.issue.pattern", value = "https://example.org/issue/{}") - @SystemProperty(name = "allure.link.tms.pattern", value = "https://example.org/tms/{}") - @Test - void shouldAddLinks() { - final AllureResults results = runFeature("features/tags.feature"); - - assertThat(results.getTestResults()) - .flatExtracting(TestResult::getLinks) - .extracting(Link::getName, Link::getType, Link::getUrl) - .contains( - tuple("OAT-4444", "tms", "https://example.org/tms/OAT-4444"), - tuple("BUG-22400", "issue", "https://example.org/issue/BUG-22400") - ); - } - - @AllureFeatures.MarkerAnnotations - @Test - void shouldAddBddLabels() { - final AllureResults results = runFeature("features/tags.feature"); - - assertThat(results.getTestResults()) - .flatExtracting(TestResult::getLabels) - .extracting(Label::getName, Label::getValue) - .contains( - tuple("feature", "Test Simple Scenarios"), - tuple("story", "Add a to b") - ); - } - - @AllureFeatures.Timeline - @Test - void shouldThreadHostLabels() { - final AllureResults results = runFeature("features/tags.feature"); - - assertThat(results.getTestResults()) - .flatExtracting(TestResult::getLabels) - .extracting(Label::getName) - .contains("host", "thread"); - } - - @AllureFeatures.MarkerAnnotations - @Test - void shouldCommonLabels() { - final AllureResults results = runFeature("features/tags.feature"); - - assertThat(results.getTestResults()) - .flatExtracting(TestResult::getLabels) - .extracting(Label::getName, Label::getValue) - .contains( - tuple("package", "Test Simple Scenarios"), - tuple("suite", "Test Simple Scenarios"), - tuple("testClass", "Add a to b") - ); - } - - @AllureFeatures.NotImplementedTests - @Test - void shouldProcessNotImplementedScenario() { - final AllureResults results = runFeature("features/undefined.feature"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.PASSED); - } - - @AllureFeatures.Base - @Test - void shouldSupportDryRun() { - final AllureResults results = runFeature("features/simple.feature", "--dry-run"); - - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getName, TestResult::getStatus) - .containsExactlyInAnyOrder( - tuple("Add a to b", Status.SKIPPED) - ); - } - - @AllureFeatures.Base - @Issue("173") - @Issue("164") - @Test - void shouldUseUuid() { - final AllureResults results = runFeature("features/simple.feature"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getUuid) - .allMatch(uuid -> nonNull(uuid) && uuid.matches("[\\-a-z0-9]+"), "UUID"); - } - - private AllureResults runFeature(final String featureResource, - final String... moreOptions) { - final AllureResultsWriterStub writer = new AllureResultsWriterStub(); - final AllureLifecycle lifecycle = new AllureLifecycle(writer); - final AllureCucumber3Jvm cucumber3Jvm = new AllureCucumber3Jvm(lifecycle); - final ClassLoader classLoader = currentThread().getContextClassLoader(); - final ResourceLoader resourceLoader = new MultiLoader(classLoader); - final ClassFinder classFinder = new ResourceLoaderClassFinder(resourceLoader, classLoader); - final List opts = new ArrayList<>(Arrays.asList( - "--glue", "io.qameta.allure.cucumber3jvm.samples", - "--plugin", "null" - )); - opts.addAll(Arrays.asList(moreOptions)); - final RuntimeOptions options = new RuntimeOptions(opts); - final Runtime runtime = new Runtime(resourceLoader, classFinder, classLoader, options); - - options.addPlugin(cucumber3Jvm); - options.noSummaryPrinter(); - - final String gherkin = readResource(featureResource); - Parser parser = new Parser<>(new AstBuilder()); - TokenMatcher matcher = new TokenMatcher(); - GherkinDocument gherkinDocument = parser.parse(gherkin, matcher); - CucumberFeature feature = new CucumberFeature(gherkinDocument, featureResource, gherkin); - - feature.sendTestSourceRead(runtime.getEventBus()); - runtime.runFeature(feature); - return writer; - } - - @AllureFeatures.Fixtures - @Test - void shouldSetStatusFailedOnBadAfter() { - final AllureResults results = runFeature("features/hooks.feature", "-t", "@bp_sf_af"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.FAILED); - } - - - @AllureFeatures.Fixtures - @Test - void shouldSetStatusSkippedOnBadBefore() { - final AllureResults results = runFeature("features/hooks.feature", "-t", "@bf_ap"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.SKIPPED); - } - - @AllureFeatures.Fixtures - @Test - void shouldSetStatusSkippedOnBadBeforeAndBadAfter() { - final AllureResults results = runFeature("features/hooks.feature", "-t", "@bf_af"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.SKIPPED); - } - - @AllureFeatures.Fixtures - @Test - void shouldSetStatusBrokenOnBadAfter() { - final AllureResults results = runFeature("features/hooks.feature", "-t", "@bp_af"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.BROKEN); - } - - @AllureFeatures.Fixtures - @Test - void shouldSetStatusFailedOnBadSteps() { - final AllureResults results = runFeature("features/hooks.feature", "-t", "@bp_sf_ap"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.FAILED); - } - - @AllureFeatures.NotImplementedTests - @Test - void shouldSetStatusBrokenOnUndefinedStepsAndBadAfter() { - final AllureResults results = runFeature("features/hooks.feature", "-t", "@bp_su_af"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.BROKEN); - } - - - @AllureFeatures.NotImplementedTests - @Test - void shouldSetStatusPassedOnUndefinedSteps() { - final AllureResults results = runFeature("features/hooks.feature", "-t", "@bp_su_ap"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.PASSED); - } - - @AllureFeatures.NotImplementedTests - @Test - void shouldSetStatusSkippedOnUndefinedAndFailedSteps() { - final AllureResults results = runFeature("features/hooks.feature", "-t", "@bp_suf_ap"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.SKIPPED); - } - - - @AllureFeatures.NotImplementedTests - @Test - void shouldSetStatusPassedOnPassedAndUndefinedSteps() { - final AllureResults results = runFeature("features/hooks.feature", "-t", "@bp_spu_ap"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.PASSED); - } - - @AllureFeatures.NotImplementedTests - @Test - void shouldSetStatusFailedOnFailedAndUndefinedSteps() { - final AllureResults results = runFeature("features/hooks.feature", "-t", "@bp_sfu_ap"); - - assertThat(results.getTestResults()) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.FAILED); - } - - private String readResource(final String resourceName) { - try (InputStream is = currentThread().getContextClassLoader().getResourceAsStream(resourceName)) { - return IOUtils.toString(is, StandardCharsets.UTF_8); - } catch (IOException e) { - throw new RuntimeException("Feature file not found " + resourceName); - } - } -} diff --git a/allure-cucumber3-jvm/src/test/resources/features/undefined.feature b/allure-cucumber3-jvm/src/test/resources/features/undefined.feature deleted file mode 100644 index caa0b507b..000000000 --- a/allure-cucumber3-jvm/src/test/resources/features/undefined.feature +++ /dev/null @@ -1,4 +0,0 @@ -Feature: Simple feature - - Scenario: Step is not implemented - Given hello my friend diff --git a/allure-cucumber4-jvm/build.gradle.kts b/allure-cucumber4-jvm/build.gradle.kts index 34e293ed8..26b244ee0 100644 --- a/allure-cucumber4-jvm/build.gradle.kts +++ b/allure-cucumber4-jvm/build.gradle.kts @@ -1,21 +1,20 @@ description = "Allure CucumberJVM 4.0" -val agent: Configuration by configurations.creating - -val cucumberVersion = "4.3.1" +val cucumberVersion = "4.8.0" dependencies { - agent("org.aspectj:aspectjweaver") api(project(":allure-java-commons")) - implementation("io.cucumber:cucumber-core:$cucumberVersion") - implementation("io.cucumber:cucumber-java:$cucumberVersion") + compileOnly("io.cucumber:cucumber-core:$cucumberVersion") + compileOnly("io.cucumber:cucumber-java:$cucumberVersion") testImplementation("commons-io:commons-io") - testImplementation("io.cucumber:cucumber-testng:$cucumberVersion") + testImplementation("io.cucumber:cucumber-core:$cucumberVersion") + testImplementation("io.cucumber:cucumber-java:$cucumberVersion") testImplementation("io.github.glytching:junit-extensions") testImplementation("org.assertj:assertj-core") 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") } @@ -29,7 +28,4 @@ tasks.jar { tasks.test { useJUnitPlatform() - doFirst { - jvmArgs("-javaagent:${agent.singleFile}") - } } diff --git a/allure-cucumber4-jvm/src/main/java/cucumber/runtime/formatter/TestSourcesModelProxy.java b/allure-cucumber4-jvm/src/main/java/cucumber/runtime/formatter/TestSourcesModelProxy.java index e6af04883..f55994b76 100644 --- a/allure-cucumber4-jvm/src/main/java/cucumber/runtime/formatter/TestSourcesModelProxy.java +++ b/allure-cucumber4-jvm/src/main/java/cucumber/runtime/formatter/TestSourcesModelProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Qameta Software OÜ + * Copyright 2016-2024 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. diff --git a/allure-cucumber4-jvm/src/main/java/io/qameta/allure/cucumber4jvm/AllureCucumber4Jvm.java b/allure-cucumber4-jvm/src/main/java/io/qameta/allure/cucumber4jvm/AllureCucumber4Jvm.java index 3dd049c7b..b5a5d429d 100644 --- a/allure-cucumber4-jvm/src/main/java/io/qameta/allure/cucumber4jvm/AllureCucumber4Jvm.java +++ b/allure-cucumber4-jvm/src/main/java/io/qameta/allure/cucumber4jvm/AllureCucumber4Jvm.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Qameta Software OÜ + * Copyright 2016-2024 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. @@ -17,10 +17,10 @@ import cucumber.api.HookTestStep; import cucumber.api.HookType; -import cucumber.api.PendingException; import cucumber.api.PickleStepTestStep; import cucumber.api.Result; import cucumber.api.TestCase; +import cucumber.api.TestStep; import cucumber.api.event.ConcurrentEventListener; import cucumber.api.event.EmbedEvent; import cucumber.api.event.EventHandler; @@ -39,6 +39,7 @@ import gherkin.ast.TableRow; import gherkin.pickles.PickleCell; import gherkin.pickles.PickleRow; +import gherkin.pickles.PickleStep; import gherkin.pickles.PickleTable; import gherkin.pickles.PickleTag; import io.qameta.allure.Allure; @@ -52,44 +53,44 @@ import io.qameta.allure.model.TestResultContainer; import java.io.ByteArrayInputStream; -import java.nio.charset.Charset; +import java.net.URI; import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; import java.util.Collections; import java.util.Deque; import java.util.LinkedList; 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.stream.Collectors; import java.util.stream.IntStream; +import java.util.stream.Stream; +import static cucumber.api.HookType.Before; import static io.qameta.allure.util.ResultsUtils.createParameter; import static io.qameta.allure.util.ResultsUtils.getStatus; import static io.qameta.allure.util.ResultsUtils.getStatusDetails; import static io.qameta.allure.util.ResultsUtils.md5; /** - * Allure plugin for Cucumber JVM 3.0. + * Allure plugin for Cucumber JVM 4.0. */ @SuppressWarnings({ - "PMD.ExcessiveImports", - "ClassFanOutComplexity", "ClassDataAbstractionCoupling" + "ClassDataAbstractionCoupling", + "ClassFanOutComplexity", + "MultipleStringLiterals", }) public class AllureCucumber4Jvm implements ConcurrentEventListener { + private static final String COLON = ":"; + private final AllureLifecycle lifecycle; - private final ConcurrentHashMap scenarioUuids = new ConcurrentHashMap<>(); private final TestSourcesModelProxy testSources = new TestSourcesModelProxy(); - private final ThreadLocal currentFeature = new InheritableThreadLocal<>(); - private final ThreadLocal currentFeatureFile = new InheritableThreadLocal<>(); - private final ThreadLocal currentTestCase = new InheritableThreadLocal<>(); - private final ThreadLocal currentContainer = new InheritableThreadLocal<>(); - private final ThreadLocal forbidTestCaseStatusChange = new InheritableThreadLocal<>(); - private final EventHandler featureStartedHandler = this::handleFeatureStartedHandler; private final EventHandler caseStartedHandler = this::handleTestCaseStarted; private final EventHandler caseFinishedHandler = this::handleTestCaseFinished; @@ -98,8 +99,14 @@ public class AllureCucumber4Jvm implements ConcurrentEventListener { private final EventHandler writeEventHandler = this::handleWriteEvent; private final EventHandler embedEventHandler = this::handleEmbedEvent; + private final Map hookStepContainerUuid = new ConcurrentHashMap<>(); + private final Map testCaseUuids = new ConcurrentHashMap<>(); + private final Map stepUuids = new ConcurrentHashMap<>(); + private final Map fixtureUuids = new ConcurrentHashMap<>(); + private static final String TXT_EXTENSION = ".txt"; private static final String TEXT_PLAIN = "text/plain"; + private static final String CUCUMBER_WORKING_DIR = Paths.get("").toUri().getSchemeSpecificPart(); @SuppressWarnings("unused") public AllureCucumber4Jvm() { @@ -124,116 +131,212 @@ public void setEventPublisher(final EventPublisher publisher) { publisher.registerHandlerFor(EmbedEvent.class, embedEventHandler); } - /* - Event Handlers - */ - private void handleFeatureStartedHandler(final TestSourceRead event) { testSources.addTestSourceReadEvent(event.uri, event); } private void handleTestCaseStarted(final TestCaseStarted event) { - currentFeatureFile.set(event.testCase.getUri()); - currentFeature.set(testSources.getFeature(currentFeatureFile.get())); - currentTestCase.set(event.testCase); - currentContainer.set(UUID.randomUUID().toString()); - forbidTestCaseStatusChange.set(false); + final TestCase testCase = event.getTestCase(); + final Feature feature = testSources.getFeature(testCase.getUri()); + + final Deque tags = new LinkedList<>(testCase.getTags()); + final LabelBuilder labelBuilder = new LabelBuilder(feature, testCase, tags); + + final String name = testCase.getName(); - final Deque tags = new LinkedList<>(currentTestCase.get().getTags()); - final LabelBuilder labelBuilder = new LabelBuilder(currentFeature.get(), currentTestCase.get(), tags); + // the same way full name is generated for + // org.junit.platform.engine.support.descriptor.ClasspathResourceSource + // to support io.qameta.allure.junitplatform.AllurePostDiscoveryFilter + final String fullName = String.format("%s:%d", + getTestCaseUri(testCase), + testCase.getLine() + ); - final String name = currentTestCase.get().getName(); - final String featureName = currentFeature.get().getName(); + final String testCaseUuid = testCaseUuids + .computeIfAbsent(testCase, tc -> UUID.randomUUID().toString()); final TestResult result = new TestResult() - .setUuid(getTestCaseUuid(currentTestCase.get())) - .setHistoryId(getHistoryId(currentTestCase.get())) - .setFullName(featureName + ": " + name) + .setUuid(testCaseUuid) + .setTestCaseId(getTestCaseId(testCase)) + .setHistoryId(getHistoryId(testCase)) + .setFullName(fullName) .setName(name) .setLabels(labelBuilder.getScenarioLabels()) .setLinks(labelBuilder.getScenarioLinks()); final ScenarioDefinition scenarioDefinition = - testSources.getScenarioDefinition(currentFeatureFile.get(), currentTestCase.get().getLine()); + testSources.getScenarioDefinition( + testCase.getUri(), + testCase.getLine() + ); + if (scenarioDefinition instanceof ScenarioOutline) { result.setParameters( - getExamplesAsParameters((ScenarioOutline) scenarioDefinition, currentTestCase.get()) + getExamplesAsParameters((ScenarioOutline) scenarioDefinition, testCase) ); } - if (currentFeature.get().getDescription() != null && !currentFeature.get().getDescription().isEmpty()) { - result.setDescription(currentFeature.get().getDescription()); - } + final String description = Stream.of(feature.getDescription(), scenarioDefinition.getDescription()) + .filter(Objects::nonNull) + .filter(s -> !s.isEmpty()) + .collect(Collectors.joining("\n")); - final TestResultContainer resultContainer = new TestResultContainer() - .setName(String.format("%s: %s", scenarioDefinition.getKeyword(), scenarioDefinition.getName())) - .setUuid(getTestContainerUuid()) - .setChildren(Collections.singletonList(getTestCaseUuid(currentTestCase.get()))); + if (!description.isEmpty()) { + result.setDescription(description); + } lifecycle.scheduleTestCase(result); - lifecycle.startTestContainer(getTestContainerUuid(), resultContainer); - lifecycle.startTestCase(getTestCaseUuid(currentTestCase.get())); + lifecycle.startTestCase(testCaseUuid); } private void handleTestCaseFinished(final TestCaseFinished event) { + final TestCase testCase = event.getTestCase(); + final String uuid = testCaseUuids.get(testCase); + if (Objects.isNull(uuid)) { + return; + } + + final Feature feature = testSources.getFeature(testCase.getUri()); + final Result result = event.result; + final Status status = translateTestCaseStatus(result); + final StatusDetails statusDetails = getStatusDetails(result.getError()) + .orElseGet(StatusDetails::new); + + final TagParser tagParser = new TagParser(feature, testCase); + statusDetails + .setFlaky(tagParser.isFlaky()) + .setMuted(tagParser.isMuted()) + .setKnown(tagParser.isKnown()); + + lifecycle.updateTestCase(uuid, testResult -> testResult + .setStatus(status) + .setStatusDetails(statusDetails) + ); - final String uuid = getTestCaseUuid(event.testCase); - final Optional details = getStatusDetails(event.result.getError()); - details.ifPresent(statusDetails -> lifecycle.updateTestCase( - uuid, - testResult -> testResult.setStatusDetails(statusDetails) - )); lifecycle.stopTestCase(uuid); - lifecycle.stopTestContainer(getTestContainerUuid()); lifecycle.writeTestCase(uuid); - lifecycle.writeTestContainer(getTestContainerUuid()); } private void handleTestStepStarted(final TestStepStarted event) { - if (event.testStep instanceof PickleStepTestStep) { - final PickleStepTestStep pickleStep = (PickleStepTestStep) event.testStep; - final String stepKeyword = Optional.ofNullable( - testSources.getKeywordFromSource(currentFeatureFile.get(), pickleStep.getStepLine()) - ).orElse("UNDEFINED"); - - final StepResult stepResult = new StepResult() - .setName(String.format("%s %s", stepKeyword, pickleStep.getPickleStep().getText())) - .setStart(System.currentTimeMillis()); - - lifecycle.startStep(getTestCaseUuid(currentTestCase.get()), getStepUuid(pickleStep), stepResult); - - pickleStep.getStepArgument().stream() - .filter(PickleTable.class::isInstance) - .findFirst() - .ifPresent(table -> createDataTableAttachment((PickleTable) table)); - } else if (event.testStep instanceof HookTestStep) { - initHook((HookTestStep) event.testStep); + final TestCase testCase = event.getTestCase(); + if (event.testStep instanceof HookTestStep) { + final HookTestStep hook = (HookTestStep) event.testStep; + + if (isFixtureHook(hook)) { + handleStartFixtureHook(testCase, hook); + } else { + handleStartStepHook(testCase, hook); + } + } else if (event.testStep instanceof PickleStepTestStep) { + handleStartPickleStep(testCase, (PickleStepTestStep) event.testStep); } } - private void initHook(final HookTestStep hook) { + private void handleStartPickleStep(final TestCase testCase, + final PickleStepTestStep pickleStep) { + final String uuid = testCaseUuids.get(testCase); + if (Objects.isNull(uuid)) { + return; + } - final FixtureResult hookResult = new FixtureResult() + final PickleStep step = pickleStep.getPickleStep(); + final String stepKeyword = Optional + .ofNullable( + testSources.getKeywordFromSource( + testCase.getUri(), + pickleStep.getStepLine() + ) + ) + .orElse(""); + + final StepResult stepResult = new StepResult() + .setName(stepKeyword + step.getText()) + .setStart(System.currentTimeMillis()); + + final String stepUuid = stepUuids.computeIfAbsent( + pickleStep, + cl -> UUID.randomUUID().toString() + ); + + lifecycle.setCurrentTestCase(uuid); + lifecycle.startStep(uuid, stepUuid, stepResult); + + pickleStep.getStepArgument() + .stream() + .filter(PickleTable.class::isInstance) + .map(PickleTable.class::cast) + .findFirst() + .ifPresent(this::createDataTableAttachment); + + } + + private void handleStartStepHook(final TestCase testCase, + final HookTestStep hook) { + final String uuid = testCaseUuids.get(testCase); + if (Objects.isNull(uuid)) { + return; + } + + final StepResult stepResult = new StepResult() .setName(hook.getCodeLocation()) .setStart(System.currentTimeMillis()); - if (hook.getHookType() == HookType.Before) { - lifecycle.startPrepareFixture(getTestContainerUuid(), getHookStepUuid(hook), hookResult); - } else { - lifecycle.startTearDownFixture(getTestContainerUuid(), getHookStepUuid(hook), hookResult); + final String stepUuid = stepUuids.computeIfAbsent( + hook, unused -> UUID.randomUUID().toString() + ); + + lifecycle.setCurrentTestCase(uuid); + lifecycle.startStep(uuid, stepUuid, stepResult); + } + + private void handleStartFixtureHook(final TestCase testCase, + final HookTestStep hook) { + final String uuid = testCaseUuids.get(testCase); + if (Objects.isNull(uuid)) { + return; } + + final String containerUuid = hookStepContainerUuid + .computeIfAbsent(hook, unused -> UUID.randomUUID().toString()); + + lifecycle.startTestContainer(new TestResultContainer() + .setUuid(containerUuid) + .setChildren(Collections.singletonList(uuid)) + ); + + final FixtureResult hookResult = new FixtureResult() + .setName(hook.getCodeLocation()); + + final String fixtureUuid = fixtureUuids.computeIfAbsent( + hook, unused -> UUID.randomUUID().toString() + ); + if (hook.getHookType() == Before) { + lifecycle.startPrepareFixture(containerUuid, fixtureUuid, hookResult); + } else { + lifecycle.startTearDownFixture(containerUuid, fixtureUuid, hookResult); + } } private void handleTestStepFinished(final TestStepFinished event) { if (event.testStep instanceof HookTestStep) { - handleHookStep(event); - } else { - handlePickleStep(event); + final HookTestStep hook = (HookTestStep) event.testStep; + if (isFixtureHook(hook)) { + handleStopHookStep(event.result, hook); + } else { + handleStopStep(event.getTestCase(), event.result, hook); + } + } else if (event.testStep instanceof PickleStepTestStep) { + final PickleStepTestStep pickleStep = (PickleStepTestStep) event.testStep; + handleStopStep(event.getTestCase(), event.result, pickleStep); } } + private static boolean isFixtureHook(final HookTestStep hook) { + return hook.getHookType() == Before || hook.getHookType() == HookType.After; + } + private void handleWriteEvent(final WriteEvent event) { lifecycle.addAttachment( "Text output", @@ -244,34 +347,41 @@ private void handleWriteEvent(final WriteEvent event) { } private void handleEmbedEvent(final EmbedEvent event) { - lifecycle.addAttachment("Screenshot", null, null, new ByteArrayInputStream(event.data)); + lifecycle.addAttachment( + Objects.isNull(event.name) + ? "Embedding" + : event.name, + event.mimeType, + null, + new ByteArrayInputStream(event.data) + ); } - /* - Utility Methods - */ - - private String getTestContainerUuid() { - return currentContainer.get(); + private String getHistoryId(final TestCase testCase) { + final String testCaseLocation = getTestCaseUri(testCase) + COLON + testCase.getLine(); + return md5(testCaseLocation); } - private String getTestCaseUuid(final TestCase testCase) { - return scenarioUuids.computeIfAbsent(getHistoryId(testCase), it -> UUID.randomUUID().toString()); + private String getTestCaseId(final TestCase testCase) { + final String testCaseId = getTestCaseUri(testCase) + COLON + testCase.getName(); + return md5(testCaseId); } - private String getStepUuid(final PickleStepTestStep step) { - return currentFeature.get().getName() + getTestCaseUuid(currentTestCase.get()) - + step.getPickleStep().getText() + step.getStepLine(); - } + private String getTestCaseUri(final TestCase testCase) { + final String testCaseUri = getUriWithoutScheme(testCase); - private String getHookStepUuid(final HookTestStep step) { - return currentFeature.get().getName() + getTestCaseUuid(currentTestCase.get()) - + step.getHookType().toString() + step.getCodeLocation(); + if (testCaseUri.startsWith(CUCUMBER_WORKING_DIR)) { + return testCaseUri.substring(CUCUMBER_WORKING_DIR.length()); + } + return testCaseUri; } - private String getHistoryId(final TestCase testCase) { - final String testCaseLocation = testCase.getUri() + ":" + testCase.getLine(); - return md5(testCaseLocation); + private static String getUriWithoutScheme(final TestCase testCase) { + try { + return URI.create(testCase.getUri()).getSchemeSpecificPart(); + } catch (Exception ignored) { + return testCase.getUri(); + } } private Status translateTestCaseStatus(final Result testCaseResult) { @@ -292,124 +402,116 @@ private Status translateTestCaseStatus(final Result testCaseResult) { } private List getExamplesAsParameters( - final ScenarioOutline scenarioOutline, final TestCase localCurrentTestCase - ) { - final Optional examplesBlock = - scenarioOutline.getExamples().stream() + final ScenarioOutline scenario, + final TestCase localCurrentTestCase) { + final Optional maybeExample = + scenario.getExamples().stream() .filter(example -> example.getTableBody().stream() - .anyMatch(row -> row.getLocation().getLine() == localCurrentTestCase.getLine()) - ).findFirst(); - - if (examplesBlock.isPresent()) { - final TableRow row = examplesBlock.get().getTableBody().stream() - .filter(example -> example.getLocation().getLine() == localCurrentTestCase.getLine()) - .findFirst().get(); - return IntStream.range(0, examplesBlock.get().getTableHeader().getCells().size()).mapToObj(index -> { - final String name = examplesBlock.get().getTableHeader().getCells().get(index).getValue(); - final String value = row.getCells().get(index).getValue(); - return createParameter(name, value); - }).collect(Collectors.toList()); - } else { + .anyMatch(row -> row.getLocation().getLine() + == localCurrentTestCase.getLine()) + ) + .findFirst(); + + if (!maybeExample.isPresent()) { + return Collections.emptyList(); + } + + final Examples examples = maybeExample.get(); + + final Optional maybeRow = examples.getTableBody().stream() + .filter(example -> example.getLocation().getLine() == localCurrentTestCase.getLine()) + .findFirst(); + + if (!maybeRow.isPresent()) { return Collections.emptyList(); } + + final TableRow row = maybeRow.get(); + + return IntStream.range(0, examples.getTableHeader().getCells().size()) + .mapToObj(index -> { + final String name = examples.getTableHeader().getCells().get(index).getValue(); + final String value = row.getCells().get(index).getValue(); + return createParameter(name, value); + }) + .collect(Collectors.toList()); } private void createDataTableAttachment(final PickleTable pickleTable) { final List rows = pickleTable.getRows(); final StringBuilder dataTableCsv = new StringBuilder(); - if (!rows.isEmpty()) { - rows.forEach(dataTableRow -> { - dataTableCsv.append( - dataTableRow.getCells().stream() - .map(PickleCell::getValue) - .collect(Collectors.joining("\t")) - ); - dataTableCsv.append('\n'); - }); - - final String attachmentSource = lifecycle - .prepareAttachment("Data table", "text/tab-separated-values", "csv"); - lifecycle.writeAttachment(attachmentSource, - new ByteArrayInputStream(dataTableCsv.toString().getBytes(Charset.forName("UTF-8")))); + for (PickleRow row : rows) { + final String rowString = row.getCells().stream() + .map(PickleCell::getValue) + .collect(Collectors.joining("\t", "", "\n")); + dataTableCsv.append(rowString); } + final String attachmentSource = lifecycle + .prepareAttachment("Data table", "text/tab-separated-values", "csv"); + lifecycle.writeAttachment(attachmentSource, + new ByteArrayInputStream(dataTableCsv.toString().getBytes(StandardCharsets.UTF_8))); } - private void handleHookStep(final TestStepFinished event) { - final HookTestStep hookStep = (HookTestStep) event.testStep; - final String uuid = getHookStepUuid(hookStep); - final FixtureResult fixtureResult = new FixtureResult().setStatus(translateTestCaseStatus(event.result)); - - if (!Status.PASSED.equals(fixtureResult.getStatus())) { - final TestResult testResult = new TestResult().setStatus(translateTestCaseStatus(event.result)); - final StatusDetails statusDetails = getStatusDetails(event.result.getError()) - .orElseGet(() -> new StatusDetails()); - - final String errorMessage = event.result.getError() == null ? hookStep.getHookType() - .name() + " is failed." : hookStep.getHookType() - .name() + " is failed: " + event.result.getError().getLocalizedMessage(); - statusDetails.setMessage(errorMessage); - - if (hookStep.getHookType() == HookType.Before) { - final TagParser tagParser = new TagParser(currentFeature.get(), currentTestCase.get()); - statusDetails - .setFlaky(tagParser.isFlaky()) - .setMuted(tagParser.isMuted()) - .setKnown(tagParser.isKnown()); - testResult.setStatus(Status.SKIPPED); - updateTestCaseStatus(testResult.getStatus()); - forbidTestCaseStatusChange.set(true); - } else { - testResult.setStatus(Status.BROKEN); - updateTestCaseStatus(testResult.getStatus()); - } - fixtureResult.setStatusDetails(statusDetails); + private void handleStopHookStep(final Result eventResult, + final HookTestStep hook) { + final String containerUuid = hookStepContainerUuid.get(hook); + if (Objects.isNull(containerUuid)) { + // maybe throw an exception? + return; + } + + final String uuid = fixtureUuids.get(hook); + if (Objects.isNull(uuid)) { + // maybe throw an exception? + return; } - lifecycle.updateFixture(uuid, result -> result.setStatus(fixtureResult.getStatus()) - .setStatusDetails(fixtureResult.getStatusDetails())); + final Status status = translateTestCaseStatus(eventResult); + final StatusDetails statusDetails = getStatusDetails(eventResult.getError()) + .orElseGet(StatusDetails::new); + + lifecycle.updateFixture(uuid, result -> result + .setStatus(status) + .setStatusDetails(statusDetails) + ); lifecycle.stopFixture(uuid); + + lifecycle.stopTestContainer(containerUuid); + lifecycle.writeTestContainer(containerUuid); } - private void handlePickleStep(final TestStepFinished event) { + private void handleStopStep(final TestCase testCase, + final Result eventResult, + final TestStep step) { + final String stepUuid = stepUuids.get(step); + if (Objects.isNull(stepUuid)) { + // maybe exception? + return; + } - final Status stepStatus = translateTestCaseStatus(event.result); - final StatusDetails statusDetails; - if (event.result.getStatus() == Result.Type.UNDEFINED) { - updateTestCaseStatus(Status.PASSED); + final Feature feature = testSources.getFeature(testCase.getUri()); - statusDetails = - getStatusDetails(new PendingException("TODO: implement me")) - .orElse(new StatusDetails()); - lifecycle.updateTestCase(getTestCaseUuid(currentTestCase.get()), scenarioResult -> - scenarioResult - .setStatusDetails(statusDetails)); - } else { - statusDetails = - getStatusDetails(event.result.getError()) - .orElse(new StatusDetails()); - updateTestCaseStatus(stepStatus); - } + final Status stepStatus = translateTestCaseStatus(eventResult); - if (!Status.PASSED.equals(stepStatus) && stepStatus != null) { - forbidTestCaseStatusChange.set(true); - } + final StatusDetails statusDetails + = eventResult.getStatus() == Result.Type.UNDEFINED + ? new StatusDetails().setMessage("Undefined Step. Please add step definition") + : getStatusDetails(eventResult.getError()) + .orElse(new StatusDetails()); - final TagParser tagParser = new TagParser(currentFeature.get(), currentTestCase.get()); + final TagParser tagParser = new TagParser(feature, testCase); statusDetails .setFlaky(tagParser.isFlaky()) .setMuted(tagParser.isMuted()) .setKnown(tagParser.isKnown()); - lifecycle.updateStep(getStepUuid((PickleStepTestStep) event.testStep), - stepResult -> stepResult.setStatus(stepStatus).setStatusDetails(statusDetails)); - lifecycle.stopStep(getStepUuid((PickleStepTestStep) event.testStep)); - } - - private void updateTestCaseStatus(final Status status) { - if (!forbidTestCaseStatusChange.get()) { - lifecycle.updateTestCase(getTestCaseUuid(currentTestCase.get()), - result -> result.setStatus(status)); - } + lifecycle.updateStep( + stepUuid, + stepResult -> stepResult + .setStatus(stepStatus) + .setStatusDetails(statusDetails) + ); + lifecycle.stopStep(stepUuid); } } diff --git a/allure-cucumber4-jvm/src/main/java/io/qameta/allure/cucumber4jvm/LabelBuilder.java b/allure-cucumber4-jvm/src/main/java/io/qameta/allure/cucumber4jvm/LabelBuilder.java index e2eef37ed..fbcbb3f10 100644 --- a/allure-cucumber4-jvm/src/main/java/io/qameta/allure/cucumber4jvm/LabelBuilder.java +++ b/allure-cucumber4-jvm/src/main/java/io/qameta/allure/cucumber4jvm/LabelBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Qameta Software OÜ + * Copyright 2016-2024 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. @@ -24,25 +24,23 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.File; import java.net.URI; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Deque; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; -import java.util.stream.StreamSupport; import static io.qameta.allure.util.ResultsUtils.createFeatureLabel; import static io.qameta.allure.util.ResultsUtils.createFrameworkLabel; import static io.qameta.allure.util.ResultsUtils.createHostLabel; import static io.qameta.allure.util.ResultsUtils.createLabel; import static io.qameta.allure.util.ResultsUtils.createLanguageLabel; -import static io.qameta.allure.util.ResultsUtils.createPackageLabel; import static io.qameta.allure.util.ResultsUtils.createStoryLabel; import static io.qameta.allure.util.ResultsUtils.createSuiteLabel; import static io.qameta.allure.util.ResultsUtils.createTestClassLabel; @@ -51,8 +49,8 @@ /** * Scenario labels and links builder. */ -@SuppressWarnings({"CyclomaticComplexity", "PMD.CyclomaticComplexity", "PMD.NcssCount", "MultipleStringLiterals"}) -class LabelBuilder { +@SuppressWarnings({"CyclomaticComplexity", "MultipleStringLiterals"}) +final class LabelBuilder { private static final Logger LOGGER = LoggerFactory.getLogger(LabelBuilder.class); private static final String COMPOSITE_TAG_DELIMITER = "="; @@ -91,44 +89,48 @@ class LabelBuilder { switch (tagKey) { case SEVERITY: - getScenarioLabels().add(ResultsUtils.createSeverityLabel(tagValue.toLowerCase())); + scenarioLabels.add(ResultsUtils.createSeverityLabel(tagValue.toLowerCase())); break; case TMS_LINK: - getScenarioLinks().add(ResultsUtils.createTmsLink(tagValue)); + scenarioLinks.add(ResultsUtils.createTmsLink(tagValue)); break; case ISSUE_LINK: - getScenarioLinks().add(ResultsUtils.createIssueLink(tagValue)); + scenarioLinks.add(ResultsUtils.createIssueLink(tagValue)); break; case PLAIN_LINK: - getScenarioLinks().add(ResultsUtils.createLink(null, tagValue, tagValue, null)); + scenarioLinks.add(ResultsUtils.createLink(null, tagValue, tagValue, null)); break; default: LOGGER.warn("Composite tag {} is not supported. adding it as RAW", tagKey); - getScenarioLabels().add(getTagLabel(tag)); + scenarioLabels.add(getTagLabel(tag)); break; } } else if (tagParser.isPureSeverityTag(tag)) { - getScenarioLabels().add(ResultsUtils.createSeverityLabel(tagString.substring(1))); + scenarioLabels.add(ResultsUtils.createSeverityLabel(tagString.substring(1))); } else if (!tagParser.isResultTag(tag)) { - getScenarioLabels().add(getTagLabel(tag)); + scenarioLabels.add(getTagLabel(tag)); } } final String featureName = feature.getName(); final String uri = scenario.getUri(); - getScenarioLabels().addAll(Arrays.asList( + scenarioLabels.addAll(ResultsUtils.getProvidedLabels()); + scenarioLabels.addAll(Arrays.asList( createHostLabel(), createThreadLabel(), createFeatureLabel(featureName), createStoryLabel(scenario.getName()), - createPackageLabel(featurePackage(uri, featureName)), createSuiteLabel(featureName), createTestClassLabel(scenario.getName()), createFrameworkLabel("cucumber4jvm"), createLanguageLabel("java"), createLabel("gherkin_uri", uri) )); + + featurePackage(uri, featureName) + .map(ResultsUtils::createPackageLabel) + .ifPresent(scenarioLabels::add); } public List