From b0d70592ffa65fe179d170b483c901f377df4012 Mon Sep 17 00:00:00 2001 From: Mohab Mohie Date: Tue, 13 Oct 2026 00:13:49 +0300 Subject: [PATCH 1/2] Add a Cucumber JVM 8 Allure adapter. The adapter mirrors allure-cucumber7-jvm and reads Gherkin lines as ints from messages v34. Cucumber 8 reports failed @BeforeAll and @AfterAll hooks on TestRunHookFinished, so those failures are stored as run-level global errors. Status tags mark flaky, muted, and known results. --- README.md | 1 + allure-bom/README.md | 4 +- allure-cucumber8-jvm/README.md | 50 + allure-cucumber8-jvm/build.gradle.kts | 30 + .../cucumber8jvm/AllureCucumber8Jvm.java | 596 +++++++++ .../allure/cucumber8jvm/LabelBuilder.java | 210 ++++ .../qameta/allure/cucumber8jvm/TagParser.java | 71 ++ .../testsourcemodel/TestSourcesModel.java | 189 +++ .../TestSourcesModelProxy.java | 110 ++ .../src/main/java/module-info.java | 24 + .../src/main/resources/META-INF/aop-ajc.xml | 7 + .../cucumber8jvm/AllureCucumber8JvmTest.java | 1065 +++++++++++++++++ .../afterall/BrokenAfterAllHooks.java | 29 + .../beforeall/BrokenBeforeAllHooks.java | 29 + .../cucumber8jvm/samples/AmbigiousSteps.java | 36 + .../cucumber8jvm/samples/AttachmentSteps.java | 39 + .../samples/BackgroundFeatureSteps.java | 39 + .../samples/BrokenFeatureSteps.java | 26 + .../samples/DatatableFeatureSteps.java | 27 + .../cucumber8jvm/samples/HookSteps.java | 60 + .../cucumber8jvm/samples/PendingSteps.java | 28 + .../cucumber8jvm/samples/RuntimeApiSteps.java | 81 ++ .../samples/SimpleFeatureSteps.java | 51 + .../src/test/resources/allure.properties | 4 + .../test/resources/features/ambigious.feature | 5 + .../resources/features/attachments.feature | 8 + .../resources/features/background.feature | 9 + .../test/resources/features/broken.feature | 4 + .../test/resources/features/datatable.feature | 8 + .../resources/features/description.feature | 17 + .../test/resources/features/examples.feature | 11 + .../test/resources/features/failed.feature | 7 + .../src/test/resources/features/hooks.feature | 98 ++ .../resources/features/metadata-tags.feature | 16 + .../resources/features/multi-examples.feature | 17 + .../test/resources/features/parallel.feature | 17 + .../test/resources/features/pending.feature | 6 + .../resources/features/runtimeapi.feature | 10 + .../resources/features/same/simple.feature | 7 + .../features/scenario_description.feature | 19 + .../test/resources/features/simple.feature | 7 + .../resources/features/status-tags.feature | 9 + .../src/test/resources/features/tags.feature | 13 + .../resources/features/undefined-last.feature | 5 + .../test/resources/features/undefined.feature | 6 + allure-jpms-tests/build.gradle.kts | 1 + settings.gradle.kts | 1 + 47 files changed, 3105 insertions(+), 2 deletions(-) create mode 100644 allure-cucumber8-jvm/README.md create mode 100644 allure-cucumber8-jvm/build.gradle.kts create mode 100644 allure-cucumber8-jvm/src/main/java/io/qameta/allure/cucumber8jvm/AllureCucumber8Jvm.java create mode 100644 allure-cucumber8-jvm/src/main/java/io/qameta/allure/cucumber8jvm/LabelBuilder.java create mode 100644 allure-cucumber8-jvm/src/main/java/io/qameta/allure/cucumber8jvm/TagParser.java create mode 100644 allure-cucumber8-jvm/src/main/java/io/qameta/allure/cucumber8jvm/testsourcemodel/TestSourcesModel.java create mode 100644 allure-cucumber8-jvm/src/main/java/io/qameta/allure/cucumber8jvm/testsourcemodel/TestSourcesModelProxy.java create mode 100644 allure-cucumber8-jvm/src/main/java/module-info.java create mode 100644 allure-cucumber8-jvm/src/main/resources/META-INF/aop-ajc.xml create mode 100644 allure-cucumber8-jvm/src/test/java/io/qameta/allure/cucumber8jvm/AllureCucumber8JvmTest.java create mode 100644 allure-cucumber8-jvm/src/test/java/io/qameta/allure/cucumber8jvm/runhooks/afterall/BrokenAfterAllHooks.java create mode 100644 allure-cucumber8-jvm/src/test/java/io/qameta/allure/cucumber8jvm/runhooks/beforeall/BrokenBeforeAllHooks.java create mode 100644 allure-cucumber8-jvm/src/test/java/io/qameta/allure/cucumber8jvm/samples/AmbigiousSteps.java create mode 100644 allure-cucumber8-jvm/src/test/java/io/qameta/allure/cucumber8jvm/samples/AttachmentSteps.java create mode 100644 allure-cucumber8-jvm/src/test/java/io/qameta/allure/cucumber8jvm/samples/BackgroundFeatureSteps.java create mode 100644 allure-cucumber8-jvm/src/test/java/io/qameta/allure/cucumber8jvm/samples/BrokenFeatureSteps.java create mode 100644 allure-cucumber8-jvm/src/test/java/io/qameta/allure/cucumber8jvm/samples/DatatableFeatureSteps.java create mode 100644 allure-cucumber8-jvm/src/test/java/io/qameta/allure/cucumber8jvm/samples/HookSteps.java create mode 100644 allure-cucumber8-jvm/src/test/java/io/qameta/allure/cucumber8jvm/samples/PendingSteps.java create mode 100644 allure-cucumber8-jvm/src/test/java/io/qameta/allure/cucumber8jvm/samples/RuntimeApiSteps.java create mode 100644 allure-cucumber8-jvm/src/test/java/io/qameta/allure/cucumber8jvm/samples/SimpleFeatureSteps.java create mode 100644 allure-cucumber8-jvm/src/test/resources/allure.properties create mode 100644 allure-cucumber8-jvm/src/test/resources/features/ambigious.feature create mode 100644 allure-cucumber8-jvm/src/test/resources/features/attachments.feature create mode 100644 allure-cucumber8-jvm/src/test/resources/features/background.feature create mode 100644 allure-cucumber8-jvm/src/test/resources/features/broken.feature create mode 100644 allure-cucumber8-jvm/src/test/resources/features/datatable.feature create mode 100644 allure-cucumber8-jvm/src/test/resources/features/description.feature create mode 100644 allure-cucumber8-jvm/src/test/resources/features/examples.feature create mode 100644 allure-cucumber8-jvm/src/test/resources/features/failed.feature create mode 100644 allure-cucumber8-jvm/src/test/resources/features/hooks.feature create mode 100644 allure-cucumber8-jvm/src/test/resources/features/metadata-tags.feature create mode 100644 allure-cucumber8-jvm/src/test/resources/features/multi-examples.feature create mode 100644 allure-cucumber8-jvm/src/test/resources/features/parallel.feature create mode 100644 allure-cucumber8-jvm/src/test/resources/features/pending.feature create mode 100644 allure-cucumber8-jvm/src/test/resources/features/runtimeapi.feature create mode 100644 allure-cucumber8-jvm/src/test/resources/features/same/simple.feature create mode 100644 allure-cucumber8-jvm/src/test/resources/features/scenario_description.feature create mode 100644 allure-cucumber8-jvm/src/test/resources/features/simple.feature create mode 100644 allure-cucumber8-jvm/src/test/resources/features/status-tags.feature create mode 100644 allure-cucumber8-jvm/src/test/resources/features/tags.feature create mode 100644 allure-cucumber8-jvm/src/test/resources/features/undefined-last.feature create mode 100644 allure-cucumber8-jvm/src/test/resources/features/undefined.feature diff --git a/README.md b/README.md index d28aa37b7..f8c1da32c 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ Run your tests, then generate or serve the report from the produced Allure resul | [`allure-spock2`](allure-spock2/README.md) | Spock 2 specifications | Spock global extension service | | [`allure-scalatest`](allure-scalatest/README.md) | ScalaTest suites | ScalaTest reporter | | [`allure-cucumber7-jvm`](allure-cucumber7-jvm/README.md) | Cucumber JVM 7 | Cucumber plugin | +| [`allure-cucumber8-jvm`](allure-cucumber8-jvm/README.md) | Cucumber JVM 8 | Cucumber plugin | | [`allure-jbehave5`](allure-jbehave5/README.md) | JBehave 5 stories | JBehave story reporter | | [`allure-karate`](allure-karate/README.md) | Karate runtime listeners | Karate runtime listener | | [`allure-citrus`](allure-citrus/README.md) | Citrus tests | Citrus listeners | diff --git a/allure-bom/README.md b/allure-bom/README.md index 59f71f014..9620e8347 100644 --- a/allure-bom/README.md +++ b/allure-bom/README.md @@ -41,7 +41,7 @@ After importing the BOM, omit versions from individual Allure Java dependencies. ## What It Aligns -- Test framework adapters such as `allure-jupiter`, `allure-testng`, and `allure-cucumber7-jvm`. +- Test framework adapters such as `allure-jupiter`, `allure-testng`, `allure-cucumber7-jvm`, and `allure-cucumber8-jvm`. - Runtime and support APIs such as `allure-java-commons` and `allure-model`. - HTTP, browser, assertion, and utility integrations. @@ -49,4 +49,4 @@ Use one Allure version for all modules in a test suite. Mixing versions can prod ## What To Expect -The BOM does not write Allure results by itself. It only controls dependency versions. Add at least one adapter, such as `allure-jupiter`, `allure-testng`, or `allure-cucumber7-jvm`, to produce report data. +The BOM does not write Allure results by itself. It only controls dependency versions. Add at least one adapter, such as `allure-jupiter`, `allure-testng`, `allure-cucumber7-jvm`, or `allure-cucumber8-jvm`, to produce report data. diff --git a/allure-cucumber8-jvm/README.md b/allure-cucumber8-jvm/README.md new file mode 100644 index 000000000..1eefabd14 --- /dev/null +++ b/allure-cucumber8-jvm/README.md @@ -0,0 +1,50 @@ +# allure-cucumber8-jvm + +Cucumber JVM 8 plugin for Allure Java. + +Use this module when your BDD tests run on Cucumber JVM 8 and you want features, scenarios, hooks, steps, tags, attachments, and examples to appear in Allure Report. + +This adapter mirrors `allure-cucumber7-jvm` and reads Gherkin line numbers from the Cucumber 8 messages API. `Location.getLine()` is a Java `int` there. Bytecode compiled against the cucumber 7 `Long getLine()` descriptor throws `NoSuchMethodError` on this runtime. + +## Supported Versions + +- Allure Java 3.x requires Java 17 or newer. +- This module targets Cucumber JVM 8.x. +- The test suite validates against Cucumber JVM 8.0.4. + +## Installation + +Gradle: + +```kotlin +dependencies { + testImplementation(platform("io.qameta.allure:allure-bom:")) + testImplementation("io.qameta.allure:allure-cucumber8-jvm") +} +``` + +Maven, with `allure-bom` imported in dependency management: + +```xml + + io.qameta.allure + allure-cucumber8-jvm + test + +``` + +## Setup + +Register the plugin with Cucumber: + +```java +@CucumberOptions(plugin = "io.qameta.allure.cucumber8jvm.AllureCucumber8Jvm") +``` + +You can also pass the plugin through your Cucumber runner or build tool configuration. + +## Report Output + +- Features, scenarios, scenario outlines, examples, hooks, and steps. +- Cucumber attachments and write events. +- Labels, links, parameters, status, and status details from tags and annotations. diff --git a/allure-cucumber8-jvm/build.gradle.kts b/allure-cucumber8-jvm/build.gradle.kts new file mode 100644 index 000000000..607b71d47 --- /dev/null +++ b/allure-cucumber8-jvm/build.gradle.kts @@ -0,0 +1,30 @@ +description = "Allure CucumberJVM 8.0" + +val cucumberVersion = "8.0.4" + +dependencies { + implementation("org.slf4j:slf4j-api") + api(project(":allure-java-commons")) + compileOnly(platform("io.cucumber:cucumber-bom:$cucumberVersion")) + compileOnly("io.cucumber:cucumber-plugin") + compileOnly("io.cucumber:gherkin") + testImplementation("commons-io:commons-io") + testImplementation(platform("io.cucumber:cucumber-bom:$cucumberVersion")) + testImplementation("io.cucumber:cucumber-core") + testImplementation("io.cucumber:cucumber-java") + testImplementation("io.cucumber:gherkin") + 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-assertj")) + testImplementation(project(":allure-java-commons-test")) + testImplementation(project(":allure-junit-platform")) + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks.test { + useJUnitPlatform() + systemProperty("allure.test.cucumber.version", cucumberVersion) +} diff --git a/allure-cucumber8-jvm/src/main/java/io/qameta/allure/cucumber8jvm/AllureCucumber8Jvm.java b/allure-cucumber8-jvm/src/main/java/io/qameta/allure/cucumber8jvm/AllureCucumber8Jvm.java new file mode 100644 index 000000000..98ade1e1c --- /dev/null +++ b/allure-cucumber8-jvm/src/main/java/io/qameta/allure/cucumber8jvm/AllureCucumber8Jvm.java @@ -0,0 +1,596 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.cucumber8jvm; + +import io.cucumber.messages.types.Envelope; +import io.cucumber.messages.types.Examples; +import io.cucumber.messages.types.Feature; +import io.cucumber.messages.types.Scenario; +import io.cucumber.messages.types.TableCell; +import io.cucumber.messages.types.TableRow; +import io.cucumber.messages.types.TestRunHookFinished; +import io.cucumber.messages.types.TestStepResultStatus; +import io.cucumber.plugin.ConcurrentEventListener; +import io.cucumber.plugin.event.DataTableArgument; +import io.cucumber.plugin.event.EmbedEvent; +import io.cucumber.plugin.event.EventHandler; +import io.cucumber.plugin.event.EventPublisher; +import io.cucumber.plugin.event.HookTestStep; +import io.cucumber.plugin.event.HookType; +import io.cucumber.plugin.event.PickleStepTestStep; +import io.cucumber.plugin.event.Result; +import io.cucumber.plugin.event.Step; +import io.cucumber.plugin.event.StepArgument; +import io.cucumber.plugin.event.TestCase; +import io.cucumber.plugin.event.TestCaseFinished; +import io.cucumber.plugin.event.TestCaseStarted; +import io.cucumber.plugin.event.TestRunFinished; +import io.cucumber.plugin.event.TestRunStarted; +import io.cucumber.plugin.event.TestSourceRead; +import io.cucumber.plugin.event.TestStepFinished; +import io.cucumber.plugin.event.TestStepStarted; +import io.cucumber.plugin.event.WriteEvent; +import io.qameta.allure.Allure; +import io.qameta.allure.AllureExternalKey; +import io.qameta.allure.AllureLifecycle; +import io.qameta.allure.AttachmentOptions; +import io.qameta.allure.cucumber8jvm.testsourcemodel.TestSourcesModelProxy; +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 java.io.ByteArrayInputStream; +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.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static io.qameta.allure.util.ResultsUtils.createGlobalError; +import static io.qameta.allure.util.ResultsUtils.createParameter; +import static io.qameta.allure.util.ResultsUtils.createTitlePath; +import static io.qameta.allure.util.ResultsUtils.createTitlePathFromSourcePath; +import static io.qameta.allure.util.ResultsUtils.getStatus; +import static io.qameta.allure.util.ResultsUtils.getStatusDetails; +import static io.qameta.allure.util.ResultsUtils.md5; + +/** + * Reports Cucumber JVM 8 execution to Allure. + * + *

Add this plugin to the Cucumber runtime so feature, scenario, step, hook, and attachment events are converted into Allure results. Use the default lifecycle for normal runs or pass one explicitly for embedded runners and tests.

+ */ +@SuppressWarnings( + { + "ClassDataAbstractionCoupling", + "ClassFanOutComplexity", + "PMD.TooManyMethods", + "PMD.GodClass", + } +) +public class AllureCucumber8Jvm implements ConcurrentEventListener { + + private static final String COLON = ":"; + private static final String CSV_DELIMITER = ","; + private static final String CSV_QUOTE = "\""; + private static final String CSV_ESCAPED_QUOTE = CSV_QUOTE + CSV_QUOTE; + private static final String NEW_LINE = "\n"; + private static final String CARRIAGE_RETURN = "\r"; + private static final String UNDEFINED_STEP_MESSAGE = "Undefined Step. Please add step definition"; + + private final AllureLifecycle lifecycle; + + private final TestSourcesModelProxy testSources = new TestSourcesModelProxy(); + + private final AtomicBoolean scenarioStarted = new AtomicBoolean(); + + private final EventHandler runStartedHandler = event -> scenarioStarted.set(false); + 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 final EventHandler runFinishedHandler = this::handleTestRunFinished; + private final EventHandler envelopeHandler = this::handleEnvelope; + + private final Map hookStepContainerUuid = new ConcurrentHashMap<>(); + + private static final String TEXT_PLAIN = "text/plain"; + private static final String CUCUMBER_WORKING_DIR = Paths.get("").toUri().getSchemeSpecificPart(); + + /** + * Creates an Allure cucumber8 jvm with default configuration. + */ + @SuppressWarnings("unused") + public AllureCucumber8Jvm() { + this(Allure.getLifecycle()); + } + + /** + * Creates an Allure cucumber8 jvm with the supplied values. + * + * @param lifecycle the Allure lifecycle to use + */ + public AllureCucumber8Jvm(final AllureLifecycle lifecycle) { + this.lifecycle = lifecycle; + } + + /** + * {@inheritDoc} + */ + @Override + public void setEventPublisher(final EventPublisher publisher) { + publisher.registerHandlerFor(TestRunStarted.class, runStartedHandler); + 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); + publisher.registerHandlerFor(TestRunFinished.class, runFinishedHandler); + // Cucumber 8 emits @BeforeAll and @AfterAll failures as messages. The + // plugin TestRunFinished result has no throwable for those hooks. + publisher.registerHandlerFor(Envelope.class, envelopeHandler); + } + + private static AllureExternalKey scopeKey(final String uuid) { + return AllureExternalKey.of(AllureCucumber8Jvm.class, "scope", uuid); + } + + private static AllureExternalKey testKey(final String uuid) { + return AllureExternalKey.of(AllureCucumber8Jvm.class, "test", uuid); + } + + private static AllureExternalKey fixtureKey(final String uuid) { + return AllureExternalKey.of(AllureCucumber8Jvm.class, "fixture", uuid); + } + + private static AllureExternalKey stepKey(final String uuid) { + return AllureExternalKey.of(AllureCucumber8Jvm.class, "step", uuid); + } + + private void handleFeatureStartedHandler(final TestSourceRead event) { + testSources.addTestSourceReadEvent(event.getUri(), event); + } + + private void handleTestCaseStarted(final TestCaseStarted event) { + scenarioStarted.set(true); + 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(); + + // 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 = getTestCaseLocation(testCase); + + final String testCaseUuid = testCase.getId().toString(); + + final List titlePath = createTitlePathFromSourcePath(getTestCaseUri(testCase)); + titlePath.addAll(createTitlePath(feature.getName())); + + final TestResult result = new TestResult() + .setUuid(testCaseUuid) + .setTestCaseId(getTestCaseId(testCase)) + .setFullName(fullName) + .setTitlePath(titlePath) + .setName(name) + .setLabels(labelBuilder.getScenarioLabels()) + .setLinks(labelBuilder.getScenarioLinks()); + + final Scenario scenarioDefinition = testSources.getScenarioDefinition( + testCase.getUri(), + testCase.getLocation().getLine() + ); + + if (scenarioDefinition.getExamples() != null) { + result.getParameters().addAll(getExamplesAsParameters(scenarioDefinition, testCase)); + } + + final String description = Stream.of(feature.getDescription(), scenarioDefinition.getDescription()) + .filter(Objects::nonNull) + .filter(s -> !s.isEmpty()) + .collect(Collectors.joining(NEW_LINE)); + + if (!description.isEmpty()) { + result.setDescription(description); + } + + final AllureExternalKey testKey = testKey(testCaseUuid); + lifecycle.scheduleTest(testKey, result); + lifecycle.addDefaultLabels(testKey, labelBuilder.getDefaultLabels()); + lifecycle.startTest(testKey); + } + + /** + * Reports an error outside an individual scenario as an error of the run. + * + *

A failed scenario also makes the run-finished result unsuccessful, but its error stays on the + * {@link TestCaseFinished} event and the run-finished result has no throwable. Only a throwable here represents + * a failure such as a run-level hook that has no scenario result of its own.

+ */ + private void handleTestRunFinished(final TestRunFinished event) { + final Result result = event.getResult(); + if (Objects.isNull(result) || Objects.isNull(result.getError())) { + return; + } + writeRunLevelError(result.getError()); + } + + /** + * Cucumber 8 returns a failed {@code @BeforeAll} or {@code @AfterAll} from the runner and emits + * {@link TestRunHookFinished} instead of attaching that throwable to {@link TestRunFinished}. + */ + private void handleEnvelope(final Envelope envelope) { + envelope.getTestRunHookFinished().ifPresent(finished -> { + if (finished.getResult().getStatus() != TestStepResultStatus.FAILED) { + return; + } + final String detail = finished.getResult().getMessage() + .or( + () -> finished.getResult().getException().flatMap( + io.cucumber.messages.types.Exception::getMessage + ) + ) + .orElse("failed"); + writeRunLevelError(new IllegalStateException(detail)); + }); + } + + private void writeRunLevelError(final Throwable failure) { + final String context = scenarioStarted.get() + ? "Cucumber test run failed" + : "Cucumber test run failed, scenarios did not run"; + lifecycle.writeGlobals(createGlobalError(context, failure)); + } + + private void handleTestCaseFinished(final TestCaseFinished event) { + final TestCase testCase = event.getTestCase(); + final Feature feature = testSources.getFeature(testCase.getUri()); + final String uuid = testCase.getId().toString(); + final Result result = event.getResult(); + final Status status = translateTestCaseStatus(result); + final StatusDetails statusDetails = translateStatusDetails(result); + + final TagParser tagParser = new TagParser(feature, testCase); + statusDetails + .setFlaky(tagParser.isFlaky()) + .setMuted(tagParser.isMuted()) + .setKnown(tagParser.isKnown()); + + final AllureExternalKey testKey = testKey(uuid); + lifecycle.updateTest( + testKey, testResult -> testResult + .setStatus(status) + .setStatusDetails(statusDetails) + ); + + lifecycle.stopTest(testKey); + lifecycle.writeTest(testKey); + } + + private void handleTestStepStarted(final TestStepStarted event) { + final TestCase testCase = event.getTestCase(); + if (event.getTestStep() instanceof HookTestStep) { + final HookTestStep hook = (HookTestStep) event.getTestStep(); + + if (isFixtureHook(hook)) { + handleStartFixtureHook(testCase, hook); + } else { + handleStartStepHook(testCase, hook); + } + } else if (event.getTestStep() instanceof PickleStepTestStep) { + handleStartPickleStep(testCase, (PickleStepTestStep) event.getTestStep()); + } + } + + private void handleStartPickleStep(final TestCase testCase, + final PickleStepTestStep pickleStep) { + final String uuid = testCase.getId().toString(); + final Step step = pickleStep.getStep(); + + final StepResult stepResult = new StepResult() + .setName(step.getKeyword() + step.getText()) + .setStart(System.currentTimeMillis()); + + final AllureExternalKey stepKey = stepKey(pickleStep.getId().toString()); + lifecycle.setCurrent(testKey(uuid)); + lifecycle.startStep(stepKey, stepResult); + + final StepArgument stepArgument = step.getArgument(); + if (stepArgument instanceof DataTableArgument) { + final DataTableArgument dataTableArgument = (DataTableArgument) stepArgument; + createDataTableAttachment(stepKey, dataTableArgument); + } + } + + private void handleStartStepHook(final TestCase testCase, + final HookTestStep hook) { + final String uuid = testCase.getId().toString(); + final StepResult stepResult = new StepResult() + .setName(hook.getCodeLocation()) + .setStart(System.currentTimeMillis()); + + lifecycle.setCurrent(testKey(uuid)); + lifecycle.startStep(stepKey(hook.getId().toString()), stepResult); + } + + private void handleStartFixtureHook(final TestCase testCase, + final HookTestStep hook) { + final String uuid = testCase.getId().toString(); + + final UUID hookId = hook.getId(); + final String containerUuid = hookStepContainerUuid + .computeIfAbsent(hookId, unused -> UUID.randomUUID().toString()); + + final AllureExternalKey scopeKey = scopeKey(containerUuid); + lifecycle.registerScope(scopeKey); + lifecycle.addTestToScope(scopeKey, testKey(uuid)); + + final FixtureResult hookResult = new FixtureResult() + .setName(hook.getCodeLocation()); + + final String fixtureUuid = hookId.toString(); + final AllureExternalKey fixtureKey = fixtureKey(fixtureUuid); + if (hook.getHookType() == HookType.BEFORE) { + lifecycle.startBeforeFixture(scopeKey, fixtureKey, hookResult); + } else { + lifecycle.startAfterFixture(scopeKey, fixtureKey, hookResult); + } + } + + private void handleTestStepFinished(final TestStepFinished event) { + if (event.getTestStep() instanceof HookTestStep) { + final HookTestStep hook = (HookTestStep) event.getTestStep(); + if (isFixtureHook(hook)) { + handleStopHookStep(event.getResult(), hook); + } else { + handleStopStep(event.getTestCase(), event.getResult(), hook.getId()); + } + } else if (event.getTestStep() instanceof PickleStepTestStep) { + final PickleStepTestStep pickleStep = (PickleStepTestStep) event.getTestStep(); + handleStopStep(event.getTestCase(), event.getResult(), pickleStep.getId()); + } + } + + private static boolean isFixtureHook(final HookTestStep hook) { + return hook.getHookType() == HookType.BEFORE || hook.getHookType() == HookType.AFTER; + } + + private void handleWriteEvent(final WriteEvent event) { + // user output is genuinely ambient: it lands under whatever executable is current, + // and is silently skipped (unsupported executables) — no key to address it by + lifecycle.getCurrentExecutableKey().ifPresent( + key -> lifecycle.addAttachment( + key, + "Text output", + TEXT_PLAIN, + new ByteArrayInputStream(Objects.toString(event.getText()).getBytes(StandardCharsets.UTF_8)), + AttachmentOptions.empty() + ) + ); + } + + private void handleEmbedEvent(final EmbedEvent event) { + lifecycle.getCurrentExecutableKey().ifPresent( + key -> lifecycle.addAttachment( + key, + event.name, + event.getMediaType(), + new ByteArrayInputStream(event.getData()), + AttachmentOptions.empty() + ) + ); + } + + private String getTestCaseLocation(final TestCase testCase) { + return getTestCaseUri(testCase) + COLON + testCase.getLocation().getLine(); + } + + private String getTestCaseId(final TestCase testCase) { + final String testCaseId = getTestCaseUri(testCase) + COLON + testCase.getName(); + return md5(testCaseId); + } + + private String getTestCaseUri(final TestCase testCase) { + final String testCaseUri = testCase.getUri().getSchemeSpecificPart(); + if (testCaseUri.startsWith(CUCUMBER_WORKING_DIR)) { + return testCaseUri.substring(CUCUMBER_WORKING_DIR.length()); + } + return testCaseUri; + } + + 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: + return Status.BROKEN; + default: + return null; + } + } + + private StatusDetails translateStatusDetails(final Result result) { + if (result.getStatus() == io.cucumber.plugin.event.Status.UNDEFINED) { + return new StatusDetails().setMessage(UNDEFINED_STEP_MESSAGE); + } + return getStatusDetails(result.getError()) + .orElseGet(StatusDetails::new); + } + + private List getExamplesAsParameters( + final Scenario scenario, + final TestCase localCurrentTestCase) { + + final Optional maybeExample = scenario.getExamples().stream() + .filter( + example -> example.getTableBody().stream() + .anyMatch(row -> row.getLocation().getLine() == localCurrentTestCase.getLocation().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.getLocation().getLine()) + .findFirst(); + + if (!maybeRow.isPresent()) { + return Collections.emptyList(); + } + + final TableRow row = maybeRow.get(); + final int size = row.getCells().size(); + + final List headerNames = examples.getTableHeader() + .map(TableRow::getCells) + .map( + rows -> rows.stream() + .map(TableCell::getValue) + .collect(Collectors.toList()) + ) + .orElse(null); + + return IntStream.range(0, size) + .mapToObj(index -> { + final String name = Objects.nonNull(headerNames) + ? headerNames.get(index) + : "arg" + index; + final String value = row.getCells().get(index).getValue(); + return createParameter(name, value); + }) + .collect(Collectors.toList()); + } + + private void createDataTableAttachment(final AllureExternalKey stepKey, + final DataTableArgument dataTableArgument) { + // the data table belongs to the step this adapter just started — address it by key + lifecycle.addAttachment( + stepKey, + "Data table", + "text/csv", + new ByteArrayInputStream(toCsv(dataTableArgument).getBytes(StandardCharsets.UTF_8)), + AttachmentOptions.empty() + ); + } + + private static String toCsv(final DataTableArgument dataTableArgument) { + final List> rowsInTable = dataTableArgument.cells(); + final StringBuilder dataTableCsv = new StringBuilder(); + for (List columns : rowsInTable) { + if (!columns.isEmpty()) { + final String rowValue = columns.stream() + .map(value -> { + final String text = Objects.toString(value, ""); + if (text.contains(CSV_DELIMITER) + || text.contains(CSV_QUOTE) + || text.contains(NEW_LINE) + || text.contains(CARRIAGE_RETURN)) { + return CSV_QUOTE + text.replace(CSV_QUOTE, CSV_ESCAPED_QUOTE) + CSV_QUOTE; + } + return text; + }) + .collect(Collectors.joining(CSV_DELIMITER, "", NEW_LINE)); + dataTableCsv.append(rowValue); + } + } + return dataTableCsv.toString(); + } + + private void handleStopHookStep(final Result eventResult, + final HookTestStep hook) { + final String containerUuid = hookStepContainerUuid.get(hook.getId()); + if (Objects.isNull(containerUuid)) { + // maybe throw an exception? + return; + } + + final String uuid = hook.getId().toString(); + + final Status status = translateTestCaseStatus(eventResult); + final StatusDetails statusDetails = getStatusDetails(eventResult.getError()) + .orElseGet(StatusDetails::new); + + final AllureExternalKey fixtureKey = fixtureKey(uuid); + lifecycle.updateFixture( + fixtureKey, result -> result + .setStatus(status) + .setStatusDetails(statusDetails) + ); + lifecycle.stopFixture(fixtureKey); + + lifecycle.writeScope(scopeKey(containerUuid)); + } + + private void handleStopStep(final TestCase testCase, + final Result eventResult, + final UUID stepId) { + final Feature feature = testSources.getFeature(testCase.getUri()); + + final Status stepStatus = translateTestCaseStatus(eventResult); + + final StatusDetails statusDetails = translateStatusDetails(eventResult); + + final TagParser tagParser = new TagParser(feature, testCase); + statusDetails + .setFlaky(tagParser.isFlaky()) + .setMuted(tagParser.isMuted()) + .setKnown(tagParser.isKnown()); + + final AllureExternalKey stepKey = stepKey(stepId.toString()); + lifecycle.updateStep( + stepKey, + stepResult -> stepResult + .setStatus(stepStatus) + .setStatusDetails(statusDetails) + ); + lifecycle.stopStep(stepKey); + } +} diff --git a/allure-cucumber8-jvm/src/main/java/io/qameta/allure/cucumber8jvm/LabelBuilder.java b/allure-cucumber8-jvm/src/main/java/io/qameta/allure/cucumber8jvm/LabelBuilder.java new file mode 100644 index 000000000..6bcaa3931 --- /dev/null +++ b/allure-cucumber8-jvm/src/main/java/io/qameta/allure/cucumber8jvm/LabelBuilder.java @@ -0,0 +1,210 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.cucumber8jvm; + +import io.cucumber.messages.types.Feature; +import io.cucumber.plugin.event.TestCase; +import io.qameta.allure.model.Label; +import io.qameta.allure.model.Link; +import io.qameta.allure.util.ResultsUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.net.URI; +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 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.createStoryLabel; +import static io.qameta.allure.util.ResultsUtils.createSuiteLabel; +import static io.qameta.allure.util.ResultsUtils.createThreadLabel; + +/** + * Scenario labels and links builder. + */ +@SuppressWarnings({"CyclomaticComplexity", "MultipleStringLiterals", "PMD.CognitiveComplexity"}) +final class LabelBuilder { + private static final Logger LOGGER = LoggerFactory.getLogger(LabelBuilder.class); + private static final String COMPOSITE_TAG_DELIMITER = "="; + + private static final String SEVERITY = "@SEVERITY"; + private static final String ISSUE_LINK = "@ISSUE"; + private static final String TMS_LINK = "@TMSLINK"; + private static final String PLAIN_LINK = "@LINK"; + private static final String OWNER = "@OWNER"; + + private final List