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 d2ec2204c..8998226d8 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 @@ -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,12 +53,14 @@ import io.qameta.allure.model.TestResultContainer; import java.io.ByteArrayInputStream; +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; @@ -66,6 +69,7 @@ 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; @@ -82,17 +86,12 @@ }) 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; @@ -101,11 +100,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().toString(); - private static final String CLASSPATH_PREFIX = "classpath:"; - private static final String FILE_PREFIX = "file:"; + private static final String CUCUMBER_WORKING_DIR = Paths.get("").toUri().getSchemeSpecificPart(); @SuppressWarnings("unused") public AllureCucumber4Jvm() { @@ -130,39 +132,46 @@ 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 TestCase testCase = currentTestCase.get(); final Deque tags = new LinkedList<>(testCase.getTags()); - - final Feature feature = currentFeature.get(); 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 = String.format("%s:%d", + getTestCaseUri(testCase), + testCase.getLine() + ); + + final String testCaseUuid = testCaseUuids + .computeIfAbsent(testCase, tc -> UUID.randomUUID().toString()); + final TestResult result = new TestResult() - .setUuid(getTestCaseUuid(testCase)) + .setUuid(testCaseUuid) + .setTestCaseId(getTestCaseId(testCase)) .setHistoryId(getHistoryId(testCase)) - .setFullName(getTestCaseUri(testCase) + ":" + testCase.getLine()) + .setFullName(fullName) .setName(name) .setLabels(labelBuilder.getScenarioLabels()) .setLinks(labelBuilder.getScenarioLinks()); final ScenarioDefinition scenarioDefinition = - testSources.getScenarioDefinition(currentFeatureFile.get(), testCase.getLine()); + testSources.getScenarioDefinition( + testCase.getUri(), + testCase.getLine() + ); + if (scenarioDefinition instanceof ScenarioOutline) { result.setParameters( getExamplesAsParameters((ScenarioOutline) scenarioDefinition, testCase) @@ -178,74 +187,157 @@ private void handleTestCaseStarted(final TestCaseStarted event) { result.setDescription(description); } - final TestResultContainer resultContainer = new TestResultContainer() - .setName(String.format("%s: %s", scenarioDefinition.getKeyword(), scenarioDefinition.getName())) - .setUuid(getTestContainerUuid()) - .setChildren(Collections.singletonList(getTestCaseUuid(testCase))); - lifecycle.scheduleTestCase(result); - lifecycle.startTestContainer(getTestContainerUuid(), resultContainer); - lifecycle.startTestCase(getTestCaseUuid(testCase)); + 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 handleStartPickleStep(final TestCase testCase, + final PickleStepTestStep pickleStep) { + final String uuid = testCaseUuids.get(testCase); + if (Objects.isNull(uuid)) { + return; } + + 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 initHook(final HookTestStep hook) { + private void handleStartStepHook(final TestCase testCase, + final HookTestStep hook) { + final String uuid = testCaseUuids.get(testCase); + if (Objects.isNull(uuid)) { + return; + } - final FixtureResult hookResult = new FixtureResult() + 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", @@ -256,34 +348,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 = getTestCaseUri(testCase) + ":" + 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) { @@ -304,139 +403,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(StandardCharsets.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(StatusDetails::new); - - 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; } - lifecycle.updateFixture(uuid, result -> result.setStatus(fixtureResult.getStatus()) - .setStatusDetails(fixtureResult.getStatusDetails())); + final String uuid = fixtureUuids.get(hook); + if (Objects.isNull(uuid)) { + // maybe throw an exception? + return; + } + + 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)); - } - } - - private String getTestCaseUri(final TestCase testCase) { - final String testCaseUri = testCase.getUri(); - if (testCaseUri.startsWith(CUCUMBER_WORKING_DIR)) { - return testCaseUri.substring(CUCUMBER_WORKING_DIR.length()); - } - if (testCaseUri.startsWith(CLASSPATH_PREFIX)) { - return testCaseUri.substring(CLASSPATH_PREFIX.length()); - } - if (testCaseUri.startsWith(FILE_PREFIX)) { - return testCaseUri.substring(FILE_PREFIX.length()); - } - return testCaseUri; + lifecycle.updateStep( + stepUuid, + stepResult -> stepResult + .setStatus(stepStatus) + .setStatusDetails(statusDetails) + ); + lifecycle.stopStep(stepUuid); } } diff --git a/allure-cucumber4-jvm/src/test/java/io/qameta/allure/cucumber4jvm/AllureCucumber4JvmTest.java b/allure-cucumber4-jvm/src/test/java/io/qameta/allure/cucumber4jvm/AllureCucumber4JvmTest.java index 18940bc83..9cc09bbfc 100644 --- a/allure-cucumber4-jvm/src/test/java/io/qameta/allure/cucumber4jvm/AllureCucumber4JvmTest.java +++ b/allure-cucumber4-jvm/src/test/java/io/qameta/allure/cucumber4jvm/AllureCucumber4JvmTest.java @@ -36,6 +36,7 @@ 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 io.qameta.allure.test.AllureFeatures; import io.qameta.allure.test.AllureResults; import io.qameta.allure.test.RunUtils; @@ -55,6 +56,7 @@ import static io.qameta.allure.util.ResultsUtils.PACKAGE_LABEL_NAME; import static io.qameta.allure.util.ResultsUtils.SUITE_LABEL_NAME; import static io.qameta.allure.util.ResultsUtils.TEST_CLASS_LABEL_NAME; +import static io.qameta.allure.util.ResultsUtils.md5; import static java.lang.Thread.currentThread; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; @@ -84,8 +86,10 @@ void shouldSetStatus() { final List testResults = results.getTestResults(); assertThat(testResults) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.PASSED); + .extracting(TestResult::getName, TestResult::getStatus) + .containsExactlyInAnyOrder( + tuple("Add a to b", Status.PASSED) + ); } @AllureFeatures.FailedTests @@ -256,7 +260,7 @@ void shouldAddAttachments() { .extracting(Attachment::getName, Attachment::getType) .containsExactlyInAnyOrder( tuple("Text output", "text/plain"), - tuple("Screenshot", null) + tuple("Embedding", "image/png") ); final List attachmentContents = results.getAttachments().values().stream() @@ -279,10 +283,10 @@ void shouldAddBackgroundSteps() { .flatExtracting(TestResult::getSteps) .extracting(StepResult::getName) .containsExactly( - "Given cat is sad", - "And cat is murmur", - "When Pet the cat", - "Then Cat is happy" + "Given cat is sad", + "And cat is murmur", + "When Pet the cat", + "Then Cat is happy" ); } @@ -444,15 +448,17 @@ void shouldProcessUndefinedSteps() { final List testResults = results.getTestResults(); assertThat(testResults) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.SKIPPED); + .extracting(TestResult::getName, TestResult::getStatus) + .containsExactlyInAnyOrder( + tuple("Step is not defined", null) + ); assertThat(testResults.get(0).getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 5", Status.PASSED), - tuple("When step is undefined", null), - tuple("Then b is 10", Status.SKIPPED) + tuple("Given a is 5", Status.PASSED), + tuple("When step is undefined", null), + tuple("Then b is 10", Status.SKIPPED) ); } @@ -470,9 +476,9 @@ void shouldProcessPendingExceptionsInSteps() { assertThat(testResults.get(0).getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 5", Status.PASSED), - tuple("When step is yet to be implemented", Status.SKIPPED), - tuple("Then b is 10", Status.SKIPPED) + tuple("Given a is 5", Status.PASSED), + tuple("When step is yet to be implemented", Status.SKIPPED), + tuple("Then b is 10", Status.SKIPPED) ); } @@ -491,10 +497,10 @@ void shouldSupportDryRunForSimpleFeatures() { assertThat(testResults.get(0).getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 5", Status.SKIPPED), - tuple("And b is 10", Status.SKIPPED), - tuple("When I add a to b", Status.SKIPPED), - tuple("Then result is 15", Status.SKIPPED) + tuple("Given a is 5", Status.SKIPPED), + tuple("And b is 10", Status.SKIPPED), + tuple("When I add a to b", Status.SKIPPED), + tuple("Then result is 15", Status.SKIPPED) ); } @@ -505,32 +511,29 @@ void shouldSupportDryRunForHooks() { final AllureResults results = runFeature("features/hooks.feature", "--dry-run", "-t", "@WithHooks or @BeforeHookWithException or @AfterHookWithException"); - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getName, TestResult::getStatus) - .startsWith( - tuple("Simple scenario with Before and After hooks", Status.SKIPPED) - ); + final TestResult tr1 = results.getTestResultByName("Simple scenario with Before and After hooks"); - assertThat(results.getTestResultContainers().get(0).getBefores()) + assertThat(results.getTestResultContainersForTestResult(tr1)) + .flatExtracting(TestResultContainer::getBefores) .extracting(FixtureResult::getName, FixtureResult::getStatus) .containsExactlyInAnyOrder( tuple("HookSteps.beforeHook()", Status.SKIPPED) ); - assertThat(results.getTestResultContainers().get(0).getAfters()) + assertThat(results.getTestResultContainersForTestResult(tr1)) + .flatExtracting(TestResultContainer::getAfters) .extracting(FixtureResult::getName, FixtureResult::getStatus) .containsExactlyInAnyOrder( tuple("HookSteps.afterHook()", Status.SKIPPED) ); - assertThat(testResults.get(0).getSteps()) + assertThat(tr1.getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 7", Status.SKIPPED), - tuple("And b is 8", Status.SKIPPED), - tuple("When I add a to b", Status.SKIPPED), - tuple("Then result is 15", Status.SKIPPED) + tuple("Given a is 7", Status.SKIPPED), + tuple("And b is 8", Status.SKIPPED), + tuple("When I add a to b", Status.SKIPPED), + tuple("Then result is 15", Status.SKIPPED) ); } @@ -552,7 +555,7 @@ void shouldPersistHistoryIdForExamples() { final List testResults = results.getTestResults(); assertThat(testResults) .extracting(TestResult::getHistoryId) - .containsExactlyInAnyOrder("646aca5d0775cd4f13161e1ea1a68c39", "c0f824814a130048e9f86358363cf23e"); + .containsExactlyInAnyOrder("c0f824814a130048e9f86358363cf23e", "646aca5d0775cd4f13161e1ea1a68c39"); } @AllureFeatures.History @@ -565,6 +568,42 @@ void shouldPersistDifferentHistoryIdComparedToTheSameTestCaseInDifferentLocation .isNotEqualTo(results2.getTestResults().get(0).getHistoryId()); } + @AllureFeatures.History + @Test + void shouldSetTestCaseIdForScenarios() { + final AllureResults results = runFeature("features/simple.feature"); + + final List testResults = results.getTestResults(); + assertThat(testResults) + .extracting(TestResult::getName, TestResult::getTestCaseId) + .containsExactlyInAnyOrder( + tuple( + "Add a to b", + md5("src/test/resources/features/simple.feature:Add a to b") + ) + ); + } + + @AllureFeatures.History + @Test + void shouldSetTestCaseIdForExamples() { + final AllureResults results = runFeature("features/examples.feature", "--threads", "2"); + + final List testResults = results.getTestResults(); + assertThat(testResults) + .extracting(TestResult::getName, TestResult::getTestCaseId) + .containsExactlyInAnyOrder( + tuple( + "Scenario with Positive Examples", + md5("src/test/resources/features/examples.feature:Scenario with Positive Examples") + ), + tuple( + "Scenario with Positive Examples", + md5("src/test/resources/features/examples.feature:Scenario with Positive Examples") + ) + ); + } + @AllureFeatures.Parallel @Test void shouldProcessScenariosInParallelMode() { @@ -576,30 +615,33 @@ void shouldProcessScenariosInParallelMode() { .hasSize(3); assertThat(testResults) - .extracting(testResult -> testResult.getSteps().stream().map(StepResult::getName).collect(Collectors.toList())) + .flatExtracting(TestResult::getSteps) + .extracting(StepResult::getName) .containsSubsequence( - Arrays.asList("Given a is 1", - "And b is 3", - "When I add a to b", - "Then result is 4") + "Given a is 1", + "And b is 3", + "When I add a to b", + "Then result is 4" ); assertThat(testResults) - .extracting(testResult -> testResult.getSteps().stream().map(StepResult::getName).collect(Collectors.toList())) + .flatExtracting(TestResult::getSteps) + .extracting(StepResult::getName) .containsSubsequence( - Arrays.asList("Given a is 2", - "And b is 4", - "When I add a to b", - "Then result is 6") + "Given a is 2", + "And b is 4", + "When I add a to b", + "Then result is 6" ); assertThat(testResults) - .extracting(testResult -> testResult.getSteps().stream().map(StepResult::getName).collect(Collectors.toList())) + .flatExtracting(TestResult::getSteps) + .extracting(StepResult::getName) .containsSubsequence( - Arrays.asList("Given a is 7", - "And b is 8", - "When I add a to b", - "Then result is 15") + "Given a is 7", + "And b is 8", + "When I add a to b", + "Then result is 15" ); } @@ -610,64 +652,61 @@ void shouldDisplayHooksAsStages() { final AllureResults results = runFeature("features/hooks.feature", "-t", "@WithHooks or @BeforeHookWithException or @AfterHookWithException"); - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getName, TestResult::getStatus) - .containsExactlyInAnyOrder( - tuple("Simple scenario with Before and After hooks", Status.PASSED), - tuple("Simple scenario with Before hook with Exception", Status.SKIPPED), - tuple("Simple scenario with After hook with Exception", Status.BROKEN) - ); + final TestResult tr1 = results.getTestResultByName("Simple scenario with Before and After hooks"); + final TestResult tr2 = results.getTestResultByName("Simple scenario with Before hook with Exception"); + final TestResult tr3 = results.getTestResultByName("Simple scenario with After hook with Exception"); - assertThat(testResults.get(0).getSteps()) + assertThat(tr1.getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 7", Status.PASSED), - tuple("And b is 8", Status.PASSED), - tuple("When I add a to b", Status.PASSED), - tuple("Then result is 15", Status.PASSED) + tuple("Given a is 7", Status.PASSED), + tuple("And b is 8", Status.PASSED), + tuple("When I add a to b", Status.PASSED), + tuple("Then result is 15", Status.PASSED) ); - - assertThat(results.getTestResultContainers().get(0).getBefores()) + assertThat(results.getTestResultContainersForTestResult(tr1)) + .flatExtracting(TestResultContainer::getBefores) .extracting(FixtureResult::getName, FixtureResult::getStatus) .containsExactlyInAnyOrder( tuple("HookSteps.beforeHook()", Status.PASSED) ); - assertThat(results.getTestResultContainers().get(0).getAfters()) + assertThat(results.getTestResultContainersForTestResult(tr1)) + .flatExtracting(TestResultContainer::getAfters) .extracting(FixtureResult::getName, FixtureResult::getStatus) .containsExactlyInAnyOrder( tuple("HookSteps.afterHook()", Status.PASSED) ); - assertThat(testResults.get(1).getSteps()) + assertThat(tr2.getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 7", Status.SKIPPED), - tuple("And b is 8", Status.SKIPPED), - tuple("When I add a to b", Status.SKIPPED), - tuple("Then result is 15", Status.SKIPPED) + tuple("Given a is 7", Status.SKIPPED), + tuple("And b is 8", Status.SKIPPED), + tuple("When I add a to b", Status.SKIPPED), + tuple("Then result is 15", Status.SKIPPED) ); - assertThat(results.getTestResultContainers().get(1).getBefores()) + assertThat(results.getTestResultContainersForTestResult(tr2)) + .flatExtracting(TestResultContainer::getBefores) .extracting(FixtureResult::getName, FixtureResult::getStatus) .containsExactlyInAnyOrder( tuple("HookSteps.beforeHookWithException()", Status.FAILED) ); - - assertThat(testResults.get(2).getSteps()) + assertThat(tr3.getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 7", Status.PASSED), - tuple("And b is 8", Status.PASSED), - tuple("When I add a to b", Status.PASSED), - tuple("Then result is 15", Status.PASSED) + tuple("Given a is 7", Status.PASSED), + tuple("And b is 8", Status.PASSED), + tuple("When I add a to b", Status.PASSED), + tuple("Then result is 15", Status.PASSED) ); - assertThat(results.getTestResultContainers().get(2).getAfters()) + assertThat(results.getTestResultContainersForTestResult(tr3)) + .flatExtracting(TestResultContainer::getAfters) .extracting(FixtureResult::getName, FixtureResult::getStatus) .containsExactlyInAnyOrder( tuple("HookSteps.afterHookWithException()", Status.FAILED) @@ -684,19 +723,19 @@ void shouldHandleAmbigiousStepsExceptions() { assertThat(testResults) .extracting(TestResult::getName, TestResult::getStatus) .containsExactlyInAnyOrder( - tuple("Simple scenario with ambigious steps", Status.SKIPPED) + tuple("Simple scenario with ambigious steps", null) ); assertThat(testResults.get(0).getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactly( - tuple("When ambigious step present", null), - tuple("Then something bad should happen", Status.SKIPPED) + tuple("When ambigious step present", null), + tuple("Then something bad should happen", Status.SKIPPED) ); } @ResourceLock(value = SYSTEM_PROPERTIES, mode = READ_WRITE) - @SystemProperty(name = "allure.label.x-provided", value = "cucumberjvm4-test-provided") + @SystemProperty(name = "allure.label.x-provided", value = "cucumberjvm5-test-provided") @Test void shouldSupportProvidedLabels() { final AllureResults results = runFeature("features/simple.feature"); @@ -707,7 +746,37 @@ void shouldSupportProvidedLabels() { .flatExtracting(TestResult::getLabels) .extracting(Label::getName, Label::getValue) .contains( - tuple("x-provided", "cucumberjvm4-test-provided") + tuple("x-provided", "cucumberjvm5-test-provided") + ); + } + + @Test + void shouldSupportRuntimeApiInStepsWhenHooksAreUsed() { + final AllureResults results = runFeature("features/runtimeapi.feature"); + + final List testResults = results.getTestResults(); + + assertThat(testResults) + .hasSize(1) + .flatExtracting(TestResult::getSteps) + .extracting(StepResult::getName) + .containsExactly( + "When step 1", + "When step 2", + "And step 3", + "Then step 4", + "And step 5" + ); + + assertThat(testResults) + .flatExtracting(TestResult::getLinks) + .extracting(Link::getName, Link::getUrl) + .containsExactly( + tuple("step1", "https://example.org/step1"), + tuple("step2", "https://example.org/step2"), + tuple("step3", "https://example.org/step3"), + tuple("step4", "https://example.org/step4"), + tuple("step5", "https://example.org/step5") ); } diff --git a/allure-cucumber4-jvm/src/test/java/io/qameta/allure/cucumber4jvm/samples/RuntimeApiSteps.java b/allure-cucumber4-jvm/src/test/java/io/qameta/allure/cucumber4jvm/samples/RuntimeApiSteps.java new file mode 100644 index 000000000..41bd4bfa6 --- /dev/null +++ b/allure-cucumber4-jvm/src/test/java/io/qameta/allure/cucumber4jvm/samples/RuntimeApiSteps.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.cucumber4jvm.samples; + +import io.cucumber.java.Before; +import io.cucumber.java.en.And; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import io.qameta.allure.Allure; + +/** + * @author charlie (Dmitry Baev). + */ +public class RuntimeApiSteps { + + @Before("@beforeScenario") + public void beforeScenario(){ + // nothing + } + + @Before("@beforeFeature") + public void beforeFeature(){ + // nothing + } + + @When("^step 1$") + public void step1() { + Allure.step("step1 nested"); + Allure.link("step1", "https://example.org/step1"); + Allure.getLifecycle().getCurrentTestCase().ifPresent(uuid -> { + System.out.println("step1: " + uuid); + }); + } + + @When("^step 2$") + public void step2() { + Allure.step("step2 nested"); + Allure.link("step2", "https://example.org/step2"); + Allure.getLifecycle().getCurrentTestCase().ifPresent(uuid -> { + System.out.println("step2: " + uuid); + }); + } + + @And("^step 3$") + public void step3() { + Allure.step("step3 nested"); + Allure.link("step3", "https://example.org/step3"); + Allure.getLifecycle().getCurrentTestCase().ifPresent(uuid -> { + System.out.println("step3: " + uuid); + }); + } + + @Then("^step 4$") + public void step4() { + Allure.step("step4 nested"); + Allure.link("step4", "https://example.org/step4"); + Allure.getLifecycle().getCurrentTestCase().ifPresent(uuid -> { + System.out.println("step4: " + uuid); + }); + } + + @And("^step 5$") + public void step5() { + Allure.step("step5 nested"); + Allure.link("step5", "https://example.org/step5"); + Allure.getLifecycle().getCurrentTestCase().ifPresent(uuid -> { + System.out.println("step5: " + uuid); + }); + } +} diff --git a/allure-cucumber4-jvm/src/test/resources/features/runtimeapi.feature b/allure-cucumber4-jvm/src/test/resources/features/runtimeapi.feature new file mode 100644 index 000000000..ec43eccb7 --- /dev/null +++ b/allure-cucumber4-jvm/src/test/resources/features/runtimeapi.feature @@ -0,0 +1,10 @@ +@beforeFeature +Feature: Should support runtime API in all steps + + @beforeScenario + Scenario: Scenario with Runtime API usage + When step 1 + When step 2 + And step 3 + Then step 4 + And step 5 diff --git a/allure-cucumber5-jvm/build.gradle.kts b/allure-cucumber5-jvm/build.gradle.kts index e39b579e4..0bb513d73 100644 --- a/allure-cucumber5-jvm/build.gradle.kts +++ b/allure-cucumber5-jvm/build.gradle.kts @@ -15,7 +15,9 @@ dependencies { 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") } diff --git a/allure-cucumber5-jvm/src/main/java/io/qameta/allure/cucumber5jvm/AllureCucumber5Jvm.java b/allure-cucumber5-jvm/src/main/java/io/qameta/allure/cucumber5jvm/AllureCucumber5Jvm.java index 928fff4d1..546838100 100644 --- a/allure-cucumber5-jvm/src/main/java/io/qameta/allure/cucumber5jvm/AllureCucumber5Jvm.java +++ b/allure-cucumber5-jvm/src/main/java/io/qameta/allure/cucumber5jvm/AllureCucumber5Jvm.java @@ -29,11 +29,13 @@ 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.TestSourceRead; +import io.cucumber.plugin.event.TestStep; import io.cucumber.plugin.event.TestStepFinished; import io.cucumber.plugin.event.TestStepStarted; import io.cucumber.plugin.event.WriteEvent; @@ -49,13 +51,13 @@ import io.qameta.allure.model.TestResultContainer; import java.io.ByteArrayInputStream; -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; @@ -80,17 +82,12 @@ }) public class AllureCucumber5Jvm 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; @@ -99,6 +96,10 @@ public class AllureCucumber5Jvm implements ConcurrentEventListener { private final EventHandler writeEventHandler = this::handleWriteEvent; private final EventHandler embedEventHandler = this::handleEmbedEvent; + private final Map hookStepContainerUuid = 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(); @@ -112,9 +113,6 @@ public AllureCucumber5Jvm(final AllureLifecycle lifecycle) { this.lifecycle = lifecycle; } - /* - Event Handlers - */ @Override public void setEventPublisher(final EventPublisher publisher) { publisher.registerHandlerFor(TestSourceRead.class, featureStartedHandler); @@ -134,30 +132,40 @@ private void handleFeatureStartedHandler(final TestSourceRead event) { } private void handleTestCaseStarted(final TestCaseStarted event) { - currentFeatureFile.set(event.getTestCase().getUri()); - currentFeature.set(testSources.getFeature(currentFeatureFile.get())); - currentTestCase.set(event.getTestCase()); - currentContainer.set(UUID.randomUUID().toString()); - forbidTestCaseStatusChange.set(false); + final TestCase testCase = event.getTestCase(); + final Feature feature = testSources.getFeature(testCase.getUri()); - final TestCase testCase = currentTestCase.get(); final Deque tags = new LinkedList<>(testCase.getTags()); - - final Feature feature = currentFeature.get(); 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 = String.format("%s:%d", + getTestCaseUri(testCase), + testCase.getLine() + ); + + final String testCaseUuid = testCase.getId().toString(); + final TestResult result = new TestResult() - .setUuid(getTestCaseUuid(testCase)) + .setUuid(testCaseUuid) + .setTestCaseId(getTestCaseId(testCase)) .setHistoryId(getHistoryId(testCase)) - .setFullName(getTestCaseUri(testCase) + ":" + testCase.getLine()) + .setFullName(fullName) .setName(name) .setLabels(labelBuilder.getScenarioLabels()) .setLinks(labelBuilder.getScenarioLinks()); final ScenarioDefinition scenarioDefinition = - testSources.getScenarioDefinition(currentFeatureFile.get(), testCase.getLine()); + testSources.getScenarioDefinition( + testCase.getUri(), + testCase.getLine() + ); + if (scenarioDefinition instanceof ScenarioOutline) { result.setParameters( getExamplesAsParameters((ScenarioOutline) scenarioDefinition, testCase) @@ -173,75 +181,131 @@ private void handleTestCaseStarted(final TestCaseStarted event) { result.setDescription(description); } - final TestResultContainer resultContainer = new TestResultContainer() - .setName(String.format("%s: %s", scenarioDefinition.getKeyword(), scenarioDefinition.getName())) - .setUuid(getTestContainerUuid()) - .setChildren(Collections.singletonList(getTestCaseUuid(testCase))); - lifecycle.scheduleTestCase(result); - lifecycle.startTestContainer(getTestContainerUuid(), resultContainer); - lifecycle.startTestCase(getTestCaseUuid(testCase)); + lifecycle.startTestCase(testCaseUuid); } 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 = 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.getTestCase()); - final Optional details = getStatusDetails(event.getResult().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.getTestStep() instanceof PickleStepTestStep) { - final PickleStepTestStep pickleStep = (PickleStepTestStep) event.getTestStep(); - final String stepKeyword = Optional.ofNullable( - testSources.getKeywordFromSource(currentFeatureFile.get(), pickleStep.getStep().getLine()) - ).orElse("UNDEFINED"); - - final StepResult stepResult = new StepResult() - .setName(String.format("%s %s", stepKeyword, pickleStep.getStep().getText())) - .setStart(System.currentTimeMillis()); - - lifecycle.startStep(getTestCaseUuid(currentTestCase.get()), getStepUuid(pickleStep), stepResult); + final TestCase testCase = event.getTestCase(); + if (event.getTestStep() instanceof HookTestStep) { + final HookTestStep hook = (HookTestStep) event.getTestStep(); - final StepArgument stepArgument = pickleStep.getStep().getArgument(); - if (stepArgument instanceof DataTableArgument) { - final DataTableArgument dataTableArgument = (DataTableArgument) stepArgument; - createDataTableAttachment(dataTableArgument); + if (isFixtureHook(hook)) { + handleStartFixtureHook(testCase, hook); + } else { + handleStartStepHook(testCase, hook); } - } else if (event.getTestStep() instanceof HookTestStep) { - initHook((HookTestStep) event.getTestStep()); + } else if (event.getTestStep() instanceof PickleStepTestStep) { + handleStartPickleStep(testCase, (PickleStepTestStep) event.getTestStep()); } } - private void initHook(final HookTestStep hook) { + private void handleStartPickleStep(final TestCase testCase, + final PickleStepTestStep pickleStep) { + final String uuid = testCase.getId().toString(); + final Step step = pickleStep.getStep(); - final FixtureResult hookResult = new FixtureResult() + final StepResult stepResult = new StepResult() + .setName(step.getKeyWord() + step.getText()) + .setStart(System.currentTimeMillis()); + + final String stepUuid = stepUuids.computeIfAbsent( + pickleStep, + cl -> UUID.randomUUID().toString() + ); + + lifecycle.setCurrentTestCase(uuid); + lifecycle.startStep(uuid, stepUuid, stepResult); + + final StepArgument stepArgument = step.getArgument(); + if (stepArgument instanceof DataTableArgument) { + final DataTableArgument dataTableArgument = (DataTableArgument) stepArgument; + createDataTableAttachment(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()); + 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 = testCase.getId().toString(); + + 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() == HookType.BEFORE) { - lifecycle.startPrepareFixture(getTestContainerUuid(), getHookStepUuid(hook), hookResult); + lifecycle.startPrepareFixture(containerUuid, fixtureUuid, hookResult); } else { - lifecycle.startTearDownFixture(getTestContainerUuid(), getHookStepUuid(hook), hookResult); + lifecycle.startTearDownFixture(containerUuid, fixtureUuid, hookResult); } - } private void handleTestStepFinished(final TestStepFinished event) { if (event.getTestStep() instanceof HookTestStep) { - handleHookStep(event); - } else { - handlePickleStep(event); + final HookTestStep hook = (HookTestStep) event.getTestStep(); + if (isFixtureHook(hook)) { + handleStopHookStep(event.getResult(), hook); + } else { + handleStopStep(event.getTestCase(), event.getResult(), hook); + } + } else if (event.getTestStep() instanceof PickleStepTestStep) { + final PickleStepTestStep pickleStep = (PickleStepTestStep) event.getTestStep(); + handleStopStep(event.getTestCase(), event.getResult(), pickleStep); } } + private static boolean isFixtureHook(final HookTestStep hook) { + return hook.getHookType() == HookType.BEFORE || hook.getHookType() == HookType.AFTER; + } + private void handleWriteEvent(final WriteEvent event) { lifecycle.addAttachment( "Text output", @@ -252,36 +316,19 @@ private void handleWriteEvent(final WriteEvent event) { } private void handleEmbedEvent(final EmbedEvent event) { - lifecycle.addAttachment("Screenshot", null, null, new ByteArrayInputStream(event.getData())); - } - - /* - Utility Methods - */ - - private String getTestContainerUuid() { - return currentContainer.get(); - } - - private String getTestCaseUuid(final TestCase testCase) { - return scenarioUuids.computeIfAbsent(getHistoryId(testCase), it -> UUID.randomUUID().toString()); - } - - private String getStepUuid(final PickleStepTestStep step) { - return currentFeature.get().getName() + getTestCaseUuid(currentTestCase.get()) - + step.getStep().getText() + step.getStep().getLine(); - } - - private String getHookStepUuid(final HookTestStep step) { - return currentFeature.get().getName() + getTestCaseUuid(currentTestCase.get()) - + step.getHookType().toString() + step.getCodeLocation(); + lifecycle.addAttachment(event.name, event.getMediaType(), null, new ByteArrayInputStream(event.getData())); } private String getHistoryId(final TestCase testCase) { - final String testCaseLocation = getTestCaseUri(testCase) + ":" + testCase.getLine(); + final String testCaseLocation = getTestCaseUri(testCase) + COLON + testCase.getLine(); return md5(testCaseLocation); } + 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)) { @@ -308,26 +355,39 @@ 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 DataTableArgument dataTableArgument) { @@ -352,83 +412,65 @@ private void createDataTableAttachment(final DataTableArgument dataTableArgument new ByteArrayInputStream(dataTableCsv.toString().getBytes(StandardCharsets.UTF_8))); } - private void handleHookStep(final TestStepFinished event) { - final HookTestStep hookStep = (HookTestStep) event.getTestStep(); - final String uuid = getHookStepUuid(hookStep); - final FixtureResult fixtureResult = new FixtureResult().setStatus(translateTestCaseStatus(event.getResult())); - - if (!Status.PASSED.equals(fixtureResult.getStatus())) { - final TestResult testResult = new TestResult().setStatus(translateTestCaseStatus(event.getResult())); - final StatusDetails statusDetails = getStatusDetails(event.getResult().getError()) - .orElseGet(StatusDetails::new); - - final String errorMessage = event.getResult().getError() == null - ? hookStep.getHookType().name() + " is failed." - : hookStep.getHookType().name() + " is failed: " - + event.getResult().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; } - lifecycle.updateFixture(uuid, result -> result.setStatus(fixtureResult.getStatus()) - .setStatusDetails(fixtureResult.getStatusDetails())); + final String uuid = fixtureUuids.get(hook); + if (Objects.isNull(uuid)) { + // maybe throw an exception? + return; + } + + 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.getResult()); - final StatusDetails statusDetails; - if (event.getResult().getStatus() == io.cucumber.plugin.event.Status.UNDEFINED) { - updateTestCaseStatus(Status.PASSED); + final Feature feature = testSources.getFeature(testCase.getUri()); - statusDetails = - getStatusDetails(new IllegalStateException("Undefined Step. Please add step definition")) - .orElse(new StatusDetails()); - lifecycle.updateTestCase(getTestCaseUuid(currentTestCase.get()), scenarioResult -> - scenarioResult - .setStatusDetails(statusDetails)); - } else { - statusDetails = - getStatusDetails(event.getResult().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() == io.cucumber.plugin.event.Status.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.getTestStep()), - stepResult -> stepResult.setStatus(stepStatus).setStatusDetails(statusDetails)); - lifecycle.stopStep(getStepUuid((PickleStepTestStep) event.getTestStep())); - } - - 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-cucumber5-jvm/src/test/java/io/qameta/allure/cucumber5jvm/AllureCucumber5JvmTest.java b/allure-cucumber5-jvm/src/test/java/io/qameta/allure/cucumber5jvm/AllureCucumber5JvmTest.java index 988882c27..f4de28513 100644 --- a/allure-cucumber5-jvm/src/test/java/io/qameta/allure/cucumber5jvm/AllureCucumber5JvmTest.java +++ b/allure-cucumber5-jvm/src/test/java/io/qameta/allure/cucumber5jvm/AllureCucumber5JvmTest.java @@ -37,6 +37,7 @@ 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 io.qameta.allure.test.AllureFeatures; import io.qameta.allure.test.AllureResults; import io.qameta.allure.test.RunUtils; @@ -59,6 +60,7 @@ import static io.qameta.allure.util.ResultsUtils.PACKAGE_LABEL_NAME; import static io.qameta.allure.util.ResultsUtils.SUITE_LABEL_NAME; import static io.qameta.allure.util.ResultsUtils.TEST_CLASS_LABEL_NAME; +import static io.qameta.allure.util.ResultsUtils.md5; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; import static org.junit.jupiter.api.parallel.ResourceAccessMode.READ_WRITE; @@ -87,8 +89,10 @@ void shouldSetStatus() { final List testResults = results.getTestResults(); assertThat(testResults) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.PASSED); + .extracting(TestResult::getName, TestResult::getStatus) + .containsExactlyInAnyOrder( + tuple("Add a to b", Status.PASSED) + ); } @AllureFeatures.FailedTests @@ -259,7 +263,7 @@ void shouldAddAttachments() { .extracting(Attachment::getName, Attachment::getType) .containsExactlyInAnyOrder( tuple("Text output", "text/plain"), - tuple("Screenshot", null) + tuple("ImageAttachment", "image/png") ); final List attachmentContents = results.getAttachments().values().stream() @@ -282,10 +286,10 @@ void shouldAddBackgroundSteps() { .flatExtracting(TestResult::getSteps) .extracting(StepResult::getName) .containsExactly( - "Given cat is sad", - "And cat is murmur", - "When Pet the cat", - "Then Cat is happy" + "Given cat is sad", + "And cat is murmur", + "When Pet the cat", + "Then Cat is happy" ); } @@ -447,15 +451,17 @@ void shouldProcessUndefinedSteps() { final List testResults = results.getTestResults(); assertThat(testResults) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.SKIPPED); + .extracting(TestResult::getName, TestResult::getStatus) + .containsExactlyInAnyOrder( + tuple("Step is not defined", null) + ); assertThat(testResults.get(0).getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 5", Status.PASSED), - tuple("When step is undefined", null), - tuple("Then b is 10", Status.SKIPPED) + tuple("Given a is 5", Status.PASSED), + tuple("When step is undefined", null), + tuple("Then b is 10", Status.SKIPPED) ); } @@ -473,9 +479,9 @@ void shouldProcessPendingExceptionsInSteps() { assertThat(testResults.get(0).getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 5", Status.PASSED), - tuple("When step is yet to be implemented", Status.SKIPPED), - tuple("Then b is 10", Status.SKIPPED) + tuple("Given a is 5", Status.PASSED), + tuple("When step is yet to be implemented", Status.SKIPPED), + tuple("Then b is 10", Status.SKIPPED) ); } @@ -494,10 +500,10 @@ void shouldSupportDryRunForSimpleFeatures() { assertThat(testResults.get(0).getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 5", Status.SKIPPED), - tuple("And b is 10", Status.SKIPPED), - tuple("When I add a to b", Status.SKIPPED), - tuple("Then result is 15", Status.SKIPPED) + tuple("Given a is 5", Status.SKIPPED), + tuple("And b is 10", Status.SKIPPED), + tuple("When I add a to b", Status.SKIPPED), + tuple("Then result is 15", Status.SKIPPED) ); } @@ -508,32 +514,29 @@ void shouldSupportDryRunForHooks() { final AllureResults results = runFeature("features/hooks.feature", "--dry-run", "-t", "@WithHooks or @BeforeHookWithException or @AfterHookWithException"); - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getName, TestResult::getStatus) - .startsWith( - tuple("Simple scenario with Before and After hooks", Status.SKIPPED) - ); + final TestResult tr1 = results.getTestResultByName("Simple scenario with Before and After hooks"); - assertThat(results.getTestResultContainers().get(0).getBefores()) + assertThat(results.getTestResultContainersForTestResult(tr1)) + .flatExtracting(TestResultContainer::getBefores) .extracting(FixtureResult::getName, FixtureResult::getStatus) .containsExactlyInAnyOrder( tuple("io.qameta.allure.cucumber5jvm.samples.HookSteps.beforeHook()", Status.SKIPPED) ); - assertThat(results.getTestResultContainers().get(0).getAfters()) + assertThat(results.getTestResultContainersForTestResult(tr1)) + .flatExtracting(TestResultContainer::getAfters) .extracting(FixtureResult::getName, FixtureResult::getStatus) .containsExactlyInAnyOrder( tuple("io.qameta.allure.cucumber5jvm.samples.HookSteps.afterHook()", Status.SKIPPED) ); - assertThat(testResults.get(0).getSteps()) + assertThat(tr1.getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 7", Status.SKIPPED), - tuple("And b is 8", Status.SKIPPED), - tuple("When I add a to b", Status.SKIPPED), - tuple("Then result is 15", Status.SKIPPED) + tuple("Given a is 7", Status.SKIPPED), + tuple("And b is 8", Status.SKIPPED), + tuple("When I add a to b", Status.SKIPPED), + tuple("Then result is 15", Status.SKIPPED) ); } @@ -568,6 +571,42 @@ void shouldPersistDifferentHistoryIdComparedToTheSameTestCaseInDifferentLocation .isNotEqualTo(results2.getTestResults().get(0).getHistoryId()); } + @AllureFeatures.History + @Test + void shouldSetTestCaseIdForScenarios() { + final AllureResults results = runFeature("features/simple.feature"); + + final List testResults = results.getTestResults(); + assertThat(testResults) + .extracting(TestResult::getName, TestResult::getTestCaseId) + .containsExactlyInAnyOrder( + tuple( + "Add a to b", + md5("src/test/resources/features/simple.feature:Add a to b") + ) + ); + } + + @AllureFeatures.History + @Test + void shouldSetTestCaseIdForExamples() { + final AllureResults results = runFeature("features/examples.feature", "--threads", "2"); + + final List testResults = results.getTestResults(); + assertThat(testResults) + .extracting(TestResult::getName, TestResult::getTestCaseId) + .containsExactlyInAnyOrder( + tuple( + "Scenario with Positive Examples", + md5("src/test/resources/features/examples.feature:Scenario with Positive Examples") + ), + tuple( + "Scenario with Positive Examples", + md5("src/test/resources/features/examples.feature:Scenario with Positive Examples") + ) + ); + } + @AllureFeatures.Parallel @Test void shouldProcessScenariosInParallelMode() { @@ -579,30 +618,33 @@ void shouldProcessScenariosInParallelMode() { .hasSize(3); assertThat(testResults) - .extracting(testResult -> testResult.getSteps().stream().map(StepResult::getName).collect(Collectors.toList())) + .flatExtracting(TestResult::getSteps) + .extracting(StepResult::getName) .containsSubsequence( - Arrays.asList("Given a is 1", - "And b is 3", - "When I add a to b", - "Then result is 4") + "Given a is 1", + "And b is 3", + "When I add a to b", + "Then result is 4" ); assertThat(testResults) - .extracting(testResult -> testResult.getSteps().stream().map(StepResult::getName).collect(Collectors.toList())) + .flatExtracting(TestResult::getSteps) + .extracting(StepResult::getName) .containsSubsequence( - Arrays.asList("Given a is 2", - "And b is 4", - "When I add a to b", - "Then result is 6") + "Given a is 2", + "And b is 4", + "When I add a to b", + "Then result is 6" ); assertThat(testResults) - .extracting(testResult -> testResult.getSteps().stream().map(StepResult::getName).collect(Collectors.toList())) + .flatExtracting(TestResult::getSteps) + .extracting(StepResult::getName) .containsSubsequence( - Arrays.asList("Given a is 7", - "And b is 8", - "When I add a to b", - "Then result is 15") + "Given a is 7", + "And b is 8", + "When I add a to b", + "Then result is 15" ); } @@ -613,64 +655,61 @@ void shouldDisplayHooksAsStages() { final AllureResults results = runFeature("features/hooks.feature", "-t", "@WithHooks or @BeforeHookWithException or @AfterHookWithException"); - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getName, TestResult::getStatus) - .containsExactlyInAnyOrder( - tuple("Simple scenario with Before and After hooks", Status.PASSED), - tuple("Simple scenario with Before hook with Exception", Status.SKIPPED), - tuple("Simple scenario with After hook with Exception", Status.BROKEN) - ); + final TestResult tr1 = results.getTestResultByName("Simple scenario with Before and After hooks"); + final TestResult tr2 = results.getTestResultByName("Simple scenario with Before hook with Exception"); + final TestResult tr3 = results.getTestResultByName("Simple scenario with After hook with Exception"); - assertThat(testResults.get(0).getSteps()) + assertThat(tr1.getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 7", Status.PASSED), - tuple("And b is 8", Status.PASSED), - tuple("When I add a to b", Status.PASSED), - tuple("Then result is 15", Status.PASSED) + tuple("Given a is 7", Status.PASSED), + tuple("And b is 8", Status.PASSED), + tuple("When I add a to b", Status.PASSED), + tuple("Then result is 15", Status.PASSED) ); - - assertThat(results.getTestResultContainers().get(0).getBefores()) + assertThat(results.getTestResultContainersForTestResult(tr1)) + .flatExtracting(TestResultContainer::getBefores) .extracting(FixtureResult::getName, FixtureResult::getStatus) .containsExactlyInAnyOrder( tuple("io.qameta.allure.cucumber5jvm.samples.HookSteps.beforeHook()", Status.PASSED) ); - assertThat(results.getTestResultContainers().get(0).getAfters()) + assertThat(results.getTestResultContainersForTestResult(tr1)) + .flatExtracting(TestResultContainer::getAfters) .extracting(FixtureResult::getName, FixtureResult::getStatus) .containsExactlyInAnyOrder( tuple("io.qameta.allure.cucumber5jvm.samples.HookSteps.afterHook()", Status.PASSED) ); - assertThat(testResults.get(1).getSteps()) + assertThat(tr2.getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 7", Status.SKIPPED), - tuple("And b is 8", Status.SKIPPED), - tuple("When I add a to b", Status.SKIPPED), - tuple("Then result is 15", Status.SKIPPED) + tuple("Given a is 7", Status.SKIPPED), + tuple("And b is 8", Status.SKIPPED), + tuple("When I add a to b", Status.SKIPPED), + tuple("Then result is 15", Status.SKIPPED) ); - assertThat(results.getTestResultContainers().get(1).getBefores()) + assertThat(results.getTestResultContainersForTestResult(tr2)) + .flatExtracting(TestResultContainer::getBefores) .extracting(FixtureResult::getName, FixtureResult::getStatus) .containsExactlyInAnyOrder( tuple("io.qameta.allure.cucumber5jvm.samples.HookSteps.beforeHookWithException()", Status.FAILED) ); - - assertThat(testResults.get(2).getSteps()) + assertThat(tr3.getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 7", Status.PASSED), - tuple("And b is 8", Status.PASSED), - tuple("When I add a to b", Status.PASSED), - tuple("Then result is 15", Status.PASSED) + tuple("Given a is 7", Status.PASSED), + tuple("And b is 8", Status.PASSED), + tuple("When I add a to b", Status.PASSED), + tuple("Then result is 15", Status.PASSED) ); - assertThat(results.getTestResultContainers().get(2).getAfters()) + assertThat(results.getTestResultContainersForTestResult(tr3)) + .flatExtracting(TestResultContainer::getAfters) .extracting(FixtureResult::getName, FixtureResult::getStatus) .containsExactlyInAnyOrder( tuple("io.qameta.allure.cucumber5jvm.samples.HookSteps.afterHookWithException()", Status.FAILED) @@ -687,14 +726,14 @@ void shouldHandleAmbigiousStepsExceptions() { assertThat(testResults) .extracting(TestResult::getName, TestResult::getStatus) .containsExactlyInAnyOrder( - tuple("Simple scenario with ambigious steps", Status.SKIPPED) + tuple("Simple scenario with ambigious steps", null) ); assertThat(testResults.get(0).getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactly( - tuple("When ambigious step present", null), - tuple("Then something bad should happen", Status.SKIPPED) + tuple("When ambigious step present", null), + tuple("Then something bad should happen", Status.SKIPPED) ); } @@ -714,6 +753,36 @@ void shouldSupportProvidedLabels() { ); } + @Test + void shouldSupportRuntimeApiInStepsWhenHooksAreUsed() { + final AllureResults results = runFeature("features/runtimeapi.feature"); + + final List testResults = results.getTestResults(); + + assertThat(testResults) + .hasSize(1) + .flatExtracting(TestResult::getSteps) + .extracting(StepResult::getName) + .containsExactly( + "When step 1", + "When step 2", + "And step 3", + "Then step 4", + "And step 5" + ); + + assertThat(testResults) + .flatExtracting(TestResult::getLinks) + .extracting(Link::getName, Link::getUrl) + .containsExactly( + tuple("step1", "https://example.org/step1"), + tuple("step2", "https://example.org/step2"), + tuple("step3", "https://example.org/step3"), + tuple("step4", "https://example.org/step4"), + tuple("step5", "https://example.org/step5") + ); + } + @SystemProperty(name = "cucumber.junit-platform.naming-strategy", value = "long") @Step private AllureResults runFeature(final String featureResource, diff --git a/allure-cucumber5-jvm/src/test/java/io/qameta/allure/cucumber5jvm/samples/RuntimeApiSteps.java b/allure-cucumber5-jvm/src/test/java/io/qameta/allure/cucumber5jvm/samples/RuntimeApiSteps.java new file mode 100644 index 000000000..20c73a7fa --- /dev/null +++ b/allure-cucumber5-jvm/src/test/java/io/qameta/allure/cucumber5jvm/samples/RuntimeApiSteps.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.cucumber5jvm.samples; + +import io.cucumber.java.Before; +import io.cucumber.java.en.And; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import io.qameta.allure.Allure; + +/** + * @author charlie (Dmitry Baev). + */ +public class RuntimeApiSteps { + + @Before("@beforeScenario") + public void beforeScenario(){ + // nothing + } + + @Before("@beforeFeature") + public void beforeFeature(){ + // nothing + } + + @When("^step 1$") + public void step1() { + Allure.step("step1 nested"); + Allure.link("step1", "https://example.org/step1"); + Allure.getLifecycle().getCurrentTestCase().ifPresent(uuid -> { + System.out.println("step1: " + uuid); + }); + } + + @When("^step 2$") + public void step2() { + Allure.step("step2 nested"); + Allure.link("step2", "https://example.org/step2"); + Allure.getLifecycle().getCurrentTestCase().ifPresent(uuid -> { + System.out.println("step2: " + uuid); + }); + } + + @And("^step 3$") + public void step3() { + Allure.step("step3 nested"); + Allure.link("step3", "https://example.org/step3"); + Allure.getLifecycle().getCurrentTestCase().ifPresent(uuid -> { + System.out.println("step3: " + uuid); + }); + } + + @Then("^step 4$") + public void step4() { + Allure.step("step4 nested"); + Allure.link("step4", "https://example.org/step4"); + Allure.getLifecycle().getCurrentTestCase().ifPresent(uuid -> { + System.out.println("step4: " + uuid); + }); + } + + @And("^step 5$") + public void step5() { + Allure.step("step5 nested"); + Allure.link("step5", "https://example.org/step5"); + Allure.getLifecycle().getCurrentTestCase().ifPresent(uuid -> { + System.out.println("step5: " + uuid); + }); + } +} diff --git a/allure-cucumber5-jvm/src/test/resources/features/runtimeapi.feature b/allure-cucumber5-jvm/src/test/resources/features/runtimeapi.feature new file mode 100644 index 000000000..ec43eccb7 --- /dev/null +++ b/allure-cucumber5-jvm/src/test/resources/features/runtimeapi.feature @@ -0,0 +1,10 @@ +@beforeFeature +Feature: Should support runtime API in all steps + + @beforeScenario + Scenario: Scenario with Runtime API usage + When step 1 + When step 2 + And step 3 + Then step 4 + And step 5 diff --git a/allure-cucumber6-jvm/src/main/java/io/qameta/allure/cucumber6jvm/AllureCucumber6Jvm.java b/allure-cucumber6-jvm/src/main/java/io/qameta/allure/cucumber6jvm/AllureCucumber6Jvm.java index fba287315..bb0e513bf 100644 --- a/allure-cucumber6-jvm/src/main/java/io/qameta/allure/cucumber6jvm/AllureCucumber6Jvm.java +++ b/allure-cucumber6-jvm/src/main/java/io/qameta/allure/cucumber6jvm/AllureCucumber6Jvm.java @@ -28,6 +28,7 @@ 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; @@ -48,13 +49,13 @@ import io.qameta.allure.model.TestResultContainer; import java.io.ByteArrayInputStream; -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; @@ -78,17 +79,12 @@ }) public class AllureCucumber6Jvm 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; @@ -97,6 +93,8 @@ public class AllureCucumber6Jvm implements ConcurrentEventListener { private final EventHandler writeEventHandler = this::handleWriteEvent; private final EventHandler embedEventHandler = this::handleEmbedEvent; + private final Map hookStepContainerUuid = 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(); @@ -110,9 +108,6 @@ public AllureCucumber6Jvm(final AllureLifecycle lifecycle) { this.lifecycle = lifecycle; } - /* - Event Handlers - */ @Override public void setEventPublisher(final EventPublisher publisher) { publisher.registerHandlerFor(TestSourceRead.class, featureStartedHandler); @@ -132,29 +127,28 @@ private void handleFeatureStartedHandler(final TestSourceRead event) { } private void handleTestCaseStarted(final TestCaseStarted event) { - currentFeatureFile.set(event.getTestCase().getUri()); - currentFeature.set(testSources.getFeature(currentFeatureFile.get())); - currentTestCase.set(event.getTestCase()); - currentContainer.set(UUID.randomUUID().toString()); - forbidTestCaseStatusChange.set(false); + final TestCase testCase = event.getTestCase(); + final Feature feature = testSources.getFeature(testCase.getUri()); - final TestCase testCase = currentTestCase.get(); final Deque tags = new LinkedList<>(testCase.getTags()); - - final Feature feature = currentFeature.get(); 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 = String.format("%s:%d", - getTestCaseUri(event.getTestCase()), - event.getTestCase().getLocation().getLine() + getTestCaseUri(testCase), + testCase.getLocation().getLine() ); + final String testCaseUuid = testCase.getId().toString(); + final TestResult result = new TestResult() - .setUuid(getTestCaseUuid(testCase)) + .setUuid(testCaseUuid) + .setTestCaseId(getTestCaseId(testCase)) .setHistoryId(getHistoryId(testCase)) .setFullName(fullName) .setName(name) @@ -163,11 +157,11 @@ private void handleTestCaseStarted(final TestCaseStarted event) { final Scenario scenarioDefinition = testSources.getScenarioDefinition( - currentFeatureFile.get(), + testCase.getUri(), testCase.getLocation().getLine() ); - if (scenarioDefinition.getExamplesCount() > 0) { + if (scenarioDefinition.getExamplesList() != null) { result.setParameters( getExamplesAsParameters(scenarioDefinition, testCase) ); @@ -182,75 +176,121 @@ private void handleTestCaseStarted(final TestCaseStarted event) { result.setDescription(description); } - final TestResultContainer resultContainer = new TestResultContainer() - .setName(String.format("%s: %s", scenarioDefinition.getKeyword(), scenarioDefinition.getName())) - .setUuid(getTestContainerUuid()) - .setChildren(Collections.singletonList(getTestCaseUuid(testCase))); - lifecycle.scheduleTestCase(result); - lifecycle.startTestContainer(getTestContainerUuid(), resultContainer); - lifecycle.startTestCase(getTestCaseUuid(testCase)); + lifecycle.startTestCase(testCaseUuid); } 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 = 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.getTestCase()); - final Optional details = getStatusDetails(event.getResult().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.getTestStep() instanceof PickleStepTestStep) { - final PickleStepTestStep pickleStep = (PickleStepTestStep) event.getTestStep(); - final String stepKeyword = Optional.ofNullable( - testSources.getKeywordFromSource(currentFeatureFile.get(), pickleStep.getStep().getLine()) - ).orElse("UNDEFINED"); - - final StepResult stepResult = new StepResult() - .setName(String.format("%s %s", stepKeyword, pickleStep.getStep().getText())) - .setStart(System.currentTimeMillis()); - - lifecycle.startStep(getTestCaseUuid(currentTestCase.get()), getStepUuid(pickleStep), stepResult); + final TestCase testCase = event.getTestCase(); + if (event.getTestStep() instanceof HookTestStep) { + final HookTestStep hook = (HookTestStep) event.getTestStep(); - final StepArgument stepArgument = pickleStep.getStep().getArgument(); - if (stepArgument instanceof DataTableArgument) { - final DataTableArgument dataTableArgument = (DataTableArgument) stepArgument; - createDataTableAttachment(dataTableArgument); + if (isFixtureHook(hook)) { + handleStartFixtureHook(testCase, hook); + } else { + handleStartStepHook(testCase, hook); } - } else if (event.getTestStep() instanceof HookTestStep) { - initHook((HookTestStep) event.getTestStep()); + } else if (event.getTestStep() instanceof PickleStepTestStep) { + handleStartPickleStep(testCase, (PickleStepTestStep) event.getTestStep()); } } - private void initHook(final HookTestStep hook) { + private void handleStartPickleStep(final TestCase testCase, + final PickleStepTestStep pickleStep) { + final String uuid = testCase.getId().toString(); + final Step step = pickleStep.getStep(); - final FixtureResult hookResult = new FixtureResult() + final StepResult stepResult = new StepResult() + .setName(step.getKeyword() + step.getText()) + .setStart(System.currentTimeMillis()); + + lifecycle.setCurrentTestCase(uuid); + lifecycle.startStep(uuid, pickleStep.getId().toString(), stepResult); + + final StepArgument stepArgument = step.getArgument(); + if (stepArgument instanceof DataTableArgument) { + final DataTableArgument dataTableArgument = (DataTableArgument) stepArgument; + createDataTableAttachment(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.setCurrentTestCase(uuid); + lifecycle.startStep(uuid, 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()); + + lifecycle.startTestContainer(new TestResultContainer() + .setUuid(containerUuid) + .setChildren(Collections.singletonList(uuid)) + ); + + final FixtureResult hookResult = new FixtureResult() + .setName(hook.getCodeLocation()); + + final String fixtureUuid = hookId.toString(); if (hook.getHookType() == HookType.BEFORE) { - lifecycle.startPrepareFixture(getTestContainerUuid(), getHookStepUuid(hook), hookResult); + lifecycle.startPrepareFixture(containerUuid, fixtureUuid, hookResult); } else { - lifecycle.startTearDownFixture(getTestContainerUuid(), getHookStepUuid(hook), hookResult); + lifecycle.startTearDownFixture(containerUuid, fixtureUuid, hookResult); } - } private void handleTestStepFinished(final TestStepFinished event) { if (event.getTestStep() instanceof HookTestStep) { - handleHookStep(event); - } else { - handlePickleStep(event); + 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) { lifecycle.addAttachment( "Text output", @@ -264,33 +304,16 @@ private void handleEmbedEvent(final EmbedEvent event) { lifecycle.addAttachment(event.name, event.getMediaType(), null, new ByteArrayInputStream(event.getData())); } - /* - Utility Methods - */ - - private String getTestContainerUuid() { - return currentContainer.get(); - } - - private String getTestCaseUuid(final TestCase testCase) { - return scenarioUuids.computeIfAbsent(getHistoryId(testCase), it -> UUID.randomUUID().toString()); - } - - private String getStepUuid(final PickleStepTestStep step) { - return currentFeature.get().getName() + getTestCaseUuid(currentTestCase.get()) - + step.getStep().getText() + step.getStep().getLine(); - } - - private String getHookStepUuid(final HookTestStep step) { - return currentFeature.get().getName() + getTestCaseUuid(currentTestCase.get()) - + step.getHookType().toString() + step.getCodeLocation(); - } - private String getHistoryId(final TestCase testCase) { - final String testCaseLocation = getTestCaseUri(testCase) + ":" + testCase.getLocation().getLine(); + final String testCaseLocation = getTestCaseUri(testCase) + COLON + testCase.getLocation().getLine(); return md5(testCaseLocation); } + 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)) { @@ -317,8 +340,8 @@ private Status translateTestCaseStatus(final Result testCaseResult) { } private List getExamplesAsParameters( - final Scenario scenario, final TestCase localCurrentTestCase - ) { + final Scenario scenario, + final TestCase localCurrentTestCase) { final Optional maybeExample = scenario.getExamplesList().stream() .filter(example -> example.getTableBodyList().stream() @@ -374,83 +397,56 @@ private void createDataTableAttachment(final DataTableArgument dataTableArgument new ByteArrayInputStream(dataTableCsv.toString().getBytes(StandardCharsets.UTF_8))); } - private void handleHookStep(final TestStepFinished event) { - final HookTestStep hookStep = (HookTestStep) event.getTestStep(); - final String uuid = getHookStepUuid(hookStep); - final FixtureResult fixtureResult = new FixtureResult().setStatus(translateTestCaseStatus(event.getResult())); - - if (!Status.PASSED.equals(fixtureResult.getStatus())) { - final TestResult testResult = new TestResult().setStatus(translateTestCaseStatus(event.getResult())); - final StatusDetails statusDetails = getStatusDetails(event.getResult().getError()) - .orElseGet(StatusDetails::new); - - final String errorMessage = event.getResult().getError() == null - ? hookStep.getHookType().name() + " is failed." - : hookStep.getHookType().name() - + " is failed: " - + event.getResult().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.getId()); + if (Objects.isNull(containerUuid)) { + // maybe throw an exception? + return; } - lifecycle.updateFixture(uuid, result -> result.setStatus(fixtureResult.getStatus()) - .setStatusDetails(fixtureResult.getStatusDetails())); + final String uuid = hook.getId().toString(); + + 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); - } - private void handlePickleStep(final TestStepFinished event) { + lifecycle.stopTestContainer(containerUuid); + lifecycle.writeTestContainer(containerUuid); + } - final Status stepStatus = translateTestCaseStatus(event.getResult()); - final StatusDetails statusDetails; - if (event.getResult().getStatus() == io.cucumber.plugin.event.Status.UNDEFINED) { - updateTestCaseStatus(Status.PASSED); + private void handleStopStep(final TestCase testCase, + final Result eventResult, + final UUID stepId) { + final Feature feature = testSources.getFeature(testCase.getUri()); - statusDetails = - getStatusDetails(new IllegalStateException("Undefined Step. Please add step definition")) - .orElse(new StatusDetails()); - lifecycle.updateTestCase(getTestCaseUuid(currentTestCase.get()), scenarioResult -> - scenarioResult - .setStatusDetails(statusDetails)); - } else { - statusDetails = - getStatusDetails(event.getResult().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() == io.cucumber.plugin.event.Status.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.getTestStep()), - stepResult -> stepResult.setStatus(stepStatus).setStatusDetails(statusDetails)); - lifecycle.stopStep(getStepUuid((PickleStepTestStep) event.getTestStep())); - } - - private void updateTestCaseStatus(final Status status) { - if (!forbidTestCaseStatusChange.get()) { - lifecycle.updateTestCase(getTestCaseUuid(currentTestCase.get()), - result -> result.setStatus(status)); - } + final String stepUuid = stepId.toString(); + lifecycle.updateStep( + stepUuid, + stepResult -> stepResult + .setStatus(stepStatus) + .setStatusDetails(statusDetails) + ); + lifecycle.stopStep(stepUuid); } } diff --git a/allure-cucumber6-jvm/src/test/java/io/qameta/allure/cucumber6jvm/AllureCucumber6JvmTest.java b/allure-cucumber6-jvm/src/test/java/io/qameta/allure/cucumber6jvm/AllureCucumber6JvmTest.java index e742aeb02..8cfbd31bf 100644 --- a/allure-cucumber6-jvm/src/test/java/io/qameta/allure/cucumber6jvm/AllureCucumber6JvmTest.java +++ b/allure-cucumber6-jvm/src/test/java/io/qameta/allure/cucumber6jvm/AllureCucumber6JvmTest.java @@ -37,6 +37,7 @@ 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 io.qameta.allure.test.AllureFeatures; import io.qameta.allure.test.AllureResults; import io.qameta.allure.test.RunUtils; @@ -59,6 +60,7 @@ import static io.qameta.allure.util.ResultsUtils.PACKAGE_LABEL_NAME; import static io.qameta.allure.util.ResultsUtils.SUITE_LABEL_NAME; import static io.qameta.allure.util.ResultsUtils.TEST_CLASS_LABEL_NAME; +import static io.qameta.allure.util.ResultsUtils.md5; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; import static org.junit.jupiter.api.parallel.ResourceAccessMode.READ_WRITE; @@ -87,8 +89,10 @@ void shouldSetStatus() { final List testResults = results.getTestResults(); assertThat(testResults) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.PASSED); + .extracting(TestResult::getName, TestResult::getStatus) + .containsExactlyInAnyOrder( + tuple("Add a to b", Status.PASSED) + ); } @AllureFeatures.FailedTests @@ -282,10 +286,10 @@ void shouldAddBackgroundSteps() { .flatExtracting(TestResult::getSteps) .extracting(StepResult::getName) .containsExactly( - "Given cat is sad", - "And cat is murmur", - "When Pet the cat", - "Then Cat is happy" + "Given cat is sad", + "And cat is murmur", + "When Pet the cat", + "Then Cat is happy" ); } @@ -447,15 +451,17 @@ void shouldProcessUndefinedSteps() { final List testResults = results.getTestResults(); assertThat(testResults) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.SKIPPED); + .extracting(TestResult::getName, TestResult::getStatus) + .containsExactlyInAnyOrder( + tuple("Step is not defined", null) + ); assertThat(testResults.get(0).getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 5", Status.PASSED), - tuple("When step is undefined", null), - tuple("Then b is 10", Status.SKIPPED) + tuple("Given a is 5", Status.PASSED), + tuple("When step is undefined", null), + tuple("Then b is 10", Status.SKIPPED) ); } @@ -473,9 +479,9 @@ void shouldProcessPendingExceptionsInSteps() { assertThat(testResults.get(0).getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 5", Status.PASSED), - tuple("When step is yet to be implemented", Status.SKIPPED), - tuple("Then b is 10", Status.SKIPPED) + tuple("Given a is 5", Status.PASSED), + tuple("When step is yet to be implemented", Status.SKIPPED), + tuple("Then b is 10", Status.SKIPPED) ); } @@ -494,10 +500,10 @@ void shouldSupportDryRunForSimpleFeatures() { assertThat(testResults.get(0).getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 5", Status.PASSED), - tuple("And b is 10", Status.PASSED), - tuple("When I add a to b", Status.PASSED), - tuple("Then result is 15", Status.PASSED) + tuple("Given a is 5", Status.PASSED), + tuple("And b is 10", Status.PASSED), + tuple("When I add a to b", Status.PASSED), + tuple("Then result is 15", Status.PASSED) ); } @@ -508,32 +514,29 @@ void shouldSupportDryRunForHooks() { final AllureResults results = runFeature("features/hooks.feature", "--dry-run", "-t", "@WithHooks or @BeforeHookWithException or @AfterHookWithException"); - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getName, TestResult::getStatus) - .startsWith( - tuple("Simple scenario with Before and After hooks", Status.PASSED) - ); + final TestResult tr1 = results.getTestResultByName("Simple scenario with Before and After hooks"); - assertThat(results.getTestResultContainers().get(0).getBefores()) + assertThat(results.getTestResultContainersForTestResult(tr1)) + .flatExtracting(TestResultContainer::getBefores) .extracting(FixtureResult::getName, FixtureResult::getStatus) .containsExactlyInAnyOrder( tuple("io.qameta.allure.cucumber6jvm.samples.HookSteps.beforeHook()", Status.PASSED) ); - assertThat(results.getTestResultContainers().get(0).getAfters()) + assertThat(results.getTestResultContainersForTestResult(tr1)) + .flatExtracting(TestResultContainer::getAfters) .extracting(FixtureResult::getName, FixtureResult::getStatus) .containsExactlyInAnyOrder( tuple("io.qameta.allure.cucumber6jvm.samples.HookSteps.afterHook()", Status.PASSED) ); - assertThat(testResults.get(0).getSteps()) + assertThat(tr1.getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 7", Status.PASSED), - tuple("And b is 8", Status.PASSED), - tuple("When I add a to b", Status.PASSED), - tuple("Then result is 15", Status.PASSED) + tuple("Given a is 7", Status.PASSED), + tuple("And b is 8", Status.PASSED), + tuple("When I add a to b", Status.PASSED), + tuple("Then result is 15", Status.PASSED) ); } @@ -568,6 +571,42 @@ void shouldPersistDifferentHistoryIdComparedToTheSameTestCaseInDifferentLocation .isNotEqualTo(results2.getTestResults().get(0).getHistoryId()); } + @AllureFeatures.History + @Test + void shouldSetTestCaseIdForScenarios() { + final AllureResults results = runFeature("features/simple.feature"); + + final List testResults = results.getTestResults(); + assertThat(testResults) + .extracting(TestResult::getName, TestResult::getTestCaseId) + .containsExactlyInAnyOrder( + tuple( + "Add a to b", + md5("src/test/resources/features/simple.feature:Add a to b") + ) + ); + } + + @AllureFeatures.History + @Test + void shouldSetTestCaseIdForExamples() { + final AllureResults results = runFeature("features/examples.feature", "--threads", "2"); + + final List testResults = results.getTestResults(); + assertThat(testResults) + .extracting(TestResult::getName, TestResult::getTestCaseId) + .containsExactlyInAnyOrder( + tuple( + "Scenario with Positive Examples", + md5("src/test/resources/features/examples.feature:Scenario with Positive Examples") + ), + tuple( + "Scenario with Positive Examples", + md5("src/test/resources/features/examples.feature:Scenario with Positive Examples") + ) + ); + } + @AllureFeatures.Parallel @Test void shouldProcessScenariosInParallelMode() { @@ -579,30 +618,33 @@ void shouldProcessScenariosInParallelMode() { .hasSize(3); assertThat(testResults) - .extracting(testResult -> testResult.getSteps().stream().map(StepResult::getName).collect(Collectors.toList())) + .flatExtracting(TestResult::getSteps) + .extracting(StepResult::getName) .containsSubsequence( - Arrays.asList("Given a is 1", - "And b is 3", - "When I add a to b", - "Then result is 4") + "Given a is 1", + "And b is 3", + "When I add a to b", + "Then result is 4" ); assertThat(testResults) - .extracting(testResult -> testResult.getSteps().stream().map(StepResult::getName).collect(Collectors.toList())) + .flatExtracting(TestResult::getSteps) + .extracting(StepResult::getName) .containsSubsequence( - Arrays.asList("Given a is 2", - "And b is 4", - "When I add a to b", - "Then result is 6") + "Given a is 2", + "And b is 4", + "When I add a to b", + "Then result is 6" ); assertThat(testResults) - .extracting(testResult -> testResult.getSteps().stream().map(StepResult::getName).collect(Collectors.toList())) + .flatExtracting(TestResult::getSteps) + .extracting(StepResult::getName) .containsSubsequence( - Arrays.asList("Given a is 7", - "And b is 8", - "When I add a to b", - "Then result is 15") + "Given a is 7", + "And b is 8", + "When I add a to b", + "Then result is 15" ); } @@ -613,64 +655,61 @@ void shouldDisplayHooksAsStages() { final AllureResults results = runFeature("features/hooks.feature", "-t", "@WithHooks or @BeforeHookWithException or @AfterHookWithException"); - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getName, TestResult::getStatus) - .containsExactlyInAnyOrder( - tuple("Simple scenario with Before and After hooks", Status.PASSED), - tuple("Simple scenario with Before hook with Exception", Status.SKIPPED), - tuple("Simple scenario with After hook with Exception", Status.BROKEN) - ); + final TestResult tr1 = results.getTestResultByName("Simple scenario with Before and After hooks"); + final TestResult tr2 = results.getTestResultByName("Simple scenario with Before hook with Exception"); + final TestResult tr3 = results.getTestResultByName("Simple scenario with After hook with Exception"); - assertThat(testResults.get(0).getSteps()) + assertThat(tr1.getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 7", Status.PASSED), - tuple("And b is 8", Status.PASSED), - tuple("When I add a to b", Status.PASSED), - tuple("Then result is 15", Status.PASSED) + tuple("Given a is 7", Status.PASSED), + tuple("And b is 8", Status.PASSED), + tuple("When I add a to b", Status.PASSED), + tuple("Then result is 15", Status.PASSED) ); - - assertThat(results.getTestResultContainers().get(0).getBefores()) + assertThat(results.getTestResultContainersForTestResult(tr1)) + .flatExtracting(TestResultContainer::getBefores) .extracting(FixtureResult::getName, FixtureResult::getStatus) .containsExactlyInAnyOrder( tuple("io.qameta.allure.cucumber6jvm.samples.HookSteps.beforeHook()", Status.PASSED) ); - assertThat(results.getTestResultContainers().get(0).getAfters()) + assertThat(results.getTestResultContainersForTestResult(tr1)) + .flatExtracting(TestResultContainer::getAfters) .extracting(FixtureResult::getName, FixtureResult::getStatus) .containsExactlyInAnyOrder( tuple("io.qameta.allure.cucumber6jvm.samples.HookSteps.afterHook()", Status.PASSED) ); - assertThat(testResults.get(1).getSteps()) + assertThat(tr2.getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 7", Status.SKIPPED), - tuple("And b is 8", Status.SKIPPED), - tuple("When I add a to b", Status.SKIPPED), - tuple("Then result is 15", Status.SKIPPED) + tuple("Given a is 7", Status.SKIPPED), + tuple("And b is 8", Status.SKIPPED), + tuple("When I add a to b", Status.SKIPPED), + tuple("Then result is 15", Status.SKIPPED) ); - assertThat(results.getTestResultContainers().get(1).getBefores()) + assertThat(results.getTestResultContainersForTestResult(tr2)) + .flatExtracting(TestResultContainer::getBefores) .extracting(FixtureResult::getName, FixtureResult::getStatus) .containsExactlyInAnyOrder( tuple("io.qameta.allure.cucumber6jvm.samples.HookSteps.beforeHookWithException()", Status.FAILED) ); - - assertThat(testResults.get(2).getSteps()) + assertThat(tr3.getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 7", Status.PASSED), - tuple("And b is 8", Status.PASSED), - tuple("When I add a to b", Status.PASSED), - tuple("Then result is 15", Status.PASSED) + tuple("Given a is 7", Status.PASSED), + tuple("And b is 8", Status.PASSED), + tuple("When I add a to b", Status.PASSED), + tuple("Then result is 15", Status.PASSED) ); - assertThat(results.getTestResultContainers().get(2).getAfters()) + assertThat(results.getTestResultContainersForTestResult(tr3)) + .flatExtracting(TestResultContainer::getAfters) .extracting(FixtureResult::getName, FixtureResult::getStatus) .containsExactlyInAnyOrder( tuple("io.qameta.allure.cucumber6jvm.samples.HookSteps.afterHookWithException()", Status.FAILED) @@ -687,14 +726,14 @@ void shouldHandleAmbigiousStepsExceptions() { assertThat(testResults) .extracting(TestResult::getName, TestResult::getStatus) .containsExactlyInAnyOrder( - tuple("Simple scenario with ambigious steps", Status.SKIPPED) + tuple("Simple scenario with ambigious steps", null) ); assertThat(testResults.get(0).getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactly( - tuple("When ambigious step present", null), - tuple("Then something bad should happen", Status.SKIPPED) + tuple("When ambigious step present", null), + tuple("Then something bad should happen", Status.SKIPPED) ); } @@ -714,6 +753,36 @@ void shouldSupportProvidedLabels() { ); } + @Test + void shouldSupportRuntimeApiInStepsWhenHooksAreUsed() { + final AllureResults results = runFeature("features/runtimeapi.feature"); + + final List testResults = results.getTestResults(); + + assertThat(testResults) + .hasSize(1) + .flatExtracting(TestResult::getSteps) + .extracting(StepResult::getName) + .containsExactly( + "When step 1", + "When step 2", + "And step 3", + "Then step 4", + "And step 5" + ); + + assertThat(testResults) + .flatExtracting(TestResult::getLinks) + .extracting(Link::getName, Link::getUrl) + .containsExactly( + tuple("step1", "https://example.org/step1"), + tuple("step2", "https://example.org/step2"), + tuple("step3", "https://example.org/step3"), + tuple("step4", "https://example.org/step4"), + tuple("step5", "https://example.org/step5") + ); + } + @SystemProperty(name = "cucumber.junit-platform.naming-strategy", value = "long") @Step private AllureResults runFeature(final String featureResource, diff --git a/allure-cucumber6-jvm/src/test/java/io/qameta/allure/cucumber6jvm/samples/RuntimeApiSteps.java b/allure-cucumber6-jvm/src/test/java/io/qameta/allure/cucumber6jvm/samples/RuntimeApiSteps.java new file mode 100644 index 000000000..c769404ef --- /dev/null +++ b/allure-cucumber6-jvm/src/test/java/io/qameta/allure/cucumber6jvm/samples/RuntimeApiSteps.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.cucumber6jvm.samples; + +import io.cucumber.java.Before; +import io.cucumber.java.en.And; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import io.qameta.allure.Allure; + +/** + * @author charlie (Dmitry Baev). + */ +public class RuntimeApiSteps { + + @Before("@beforeScenario") + public void beforeScenario(){ + // nothing + } + + @Before("@beforeFeature") + public void beforeFeature(){ + // nothing + } + + @When("^step 1$") + public void step1() { + Allure.step("step1 nested"); + Allure.link("step1", "https://example.org/step1"); + Allure.getLifecycle().getCurrentTestCase().ifPresent(uuid -> { + System.out.println("step1: " + uuid); + }); + } + + @When("^step 2$") + public void step2() { + Allure.step("step2 nested"); + Allure.link("step2", "https://example.org/step2"); + Allure.getLifecycle().getCurrentTestCase().ifPresent(uuid -> { + System.out.println("step2: " + uuid); + }); + } + + @And("^step 3$") + public void step3() { + Allure.step("step3 nested"); + Allure.link("step3", "https://example.org/step3"); + Allure.getLifecycle().getCurrentTestCase().ifPresent(uuid -> { + System.out.println("step3: " + uuid); + }); + } + + @Then("^step 4$") + public void step4() { + Allure.step("step4 nested"); + Allure.link("step4", "https://example.org/step4"); + Allure.getLifecycle().getCurrentTestCase().ifPresent(uuid -> { + System.out.println("step4: " + uuid); + }); + } + + @And("^step 5$") + public void step5() { + Allure.step("step5 nested"); + Allure.link("step5", "https://example.org/step5"); + Allure.getLifecycle().getCurrentTestCase().ifPresent(uuid -> { + System.out.println("step5: " + uuid); + }); + } +} diff --git a/allure-cucumber6-jvm/src/test/resources/features/runtimeapi.feature b/allure-cucumber6-jvm/src/test/resources/features/runtimeapi.feature new file mode 100644 index 000000000..ec43eccb7 --- /dev/null +++ b/allure-cucumber6-jvm/src/test/resources/features/runtimeapi.feature @@ -0,0 +1,10 @@ +@beforeFeature +Feature: Should support runtime API in all steps + + @beforeScenario + Scenario: Scenario with Runtime API usage + When step 1 + When step 2 + And step 3 + Then step 4 + And step 5 diff --git a/allure-cucumber7-jvm/src/main/java/io/qameta/allure/cucumber7jvm/AllureCucumber7Jvm.java b/allure-cucumber7-jvm/src/main/java/io/qameta/allure/cucumber7jvm/AllureCucumber7Jvm.java index f5cf815e4..5f85512fd 100644 --- a/allure-cucumber7-jvm/src/main/java/io/qameta/allure/cucumber7jvm/AllureCucumber7Jvm.java +++ b/allure-cucumber7-jvm/src/main/java/io/qameta/allure/cucumber7jvm/AllureCucumber7Jvm.java @@ -18,6 +18,7 @@ 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.plugin.ConcurrentEventListener; import io.cucumber.plugin.event.DataTableArgument; @@ -28,6 +29,7 @@ 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; @@ -48,13 +50,13 @@ import io.qameta.allure.model.TestResultContainer; import java.io.ByteArrayInputStream; -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; @@ -78,17 +80,12 @@ }) public class AllureCucumber7Jvm 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; @@ -97,6 +94,8 @@ public class AllureCucumber7Jvm implements ConcurrentEventListener { private final EventHandler writeEventHandler = this::handleWriteEvent; private final EventHandler embedEventHandler = this::handleEmbedEvent; + private final Map hookStepContainerUuid = 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(); @@ -110,9 +109,6 @@ public AllureCucumber7Jvm(final AllureLifecycle lifecycle) { this.lifecycle = lifecycle; } - /* - Event Handlers - */ @Override public void setEventPublisher(final EventPublisher publisher) { publisher.registerHandlerFor(TestSourceRead.class, featureStartedHandler); @@ -132,20 +128,15 @@ private void handleFeatureStartedHandler(final TestSourceRead event) { } private void handleTestCaseStarted(final TestCaseStarted event) { - currentFeatureFile.set(event.getTestCase().getUri()); - currentFeature.set(testSources.getFeature(currentFeatureFile.get())); - currentTestCase.set(event.getTestCase()); - currentContainer.set(UUID.randomUUID().toString()); - forbidTestCaseStatusChange.set(false); + final TestCase testCase = event.getTestCase(); + final Feature feature = testSources.getFeature(testCase.getUri()); - final TestCase testCase = currentTestCase.get(); final Deque tags = new LinkedList<>(testCase.getTags()); - - final Feature feature = currentFeature.get(); 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 @@ -154,8 +145,11 @@ private void handleTestCaseStarted(final TestCaseStarted event) { testCase.getLocation().getLine() ); + final String testCaseUuid = testCase.getId().toString(); + final TestResult result = new TestResult() - .setUuid(getTestCaseUuid(testCase)) + .setUuid(testCaseUuid) + .setTestCaseId(getTestCaseId(testCase)) .setHistoryId(getHistoryId(testCase)) .setFullName(fullName) .setName(name) @@ -164,7 +158,7 @@ private void handleTestCaseStarted(final TestCaseStarted event) { final Scenario scenarioDefinition = testSources.getScenarioDefinition( - currentFeatureFile.get(), + testCase.getUri(), testCase.getLocation().getLine() ); @@ -183,75 +177,121 @@ private void handleTestCaseStarted(final TestCaseStarted event) { result.setDescription(description); } - final TestResultContainer resultContainer = new TestResultContainer() - .setName(String.format("%s: %s", scenarioDefinition.getKeyword(), scenarioDefinition.getName())) - .setUuid(getTestContainerUuid()) - .setChildren(Collections.singletonList(getTestCaseUuid(testCase))); - lifecycle.scheduleTestCase(result); - lifecycle.startTestContainer(getTestContainerUuid(), resultContainer); - lifecycle.startTestCase(getTestCaseUuid(testCase)); + lifecycle.startTestCase(testCaseUuid); } 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 = 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.getTestCase()); - final Optional details = getStatusDetails(event.getResult().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.getTestStep() instanceof PickleStepTestStep) { - final PickleStepTestStep pickleStep = (PickleStepTestStep) event.getTestStep(); - final String stepKeyword = Optional.ofNullable( - testSources.getKeywordFromSource(currentFeatureFile.get(), pickleStep.getStep().getLine()) - ).orElse("UNDEFINED"); - - final StepResult stepResult = new StepResult() - .setName(String.format("%s %s", stepKeyword, pickleStep.getStep().getText())) - .setStart(System.currentTimeMillis()); - - lifecycle.startStep(getTestCaseUuid(currentTestCase.get()), getStepUuid(pickleStep), stepResult); + final TestCase testCase = event.getTestCase(); + if (event.getTestStep() instanceof HookTestStep) { + final HookTestStep hook = (HookTestStep) event.getTestStep(); - final StepArgument stepArgument = pickleStep.getStep().getArgument(); - if (stepArgument instanceof DataTableArgument) { - final DataTableArgument dataTableArgument = (DataTableArgument) stepArgument; - createDataTableAttachment(dataTableArgument); + if (isFixtureHook(hook)) { + handleStartFixtureHook(testCase, hook); + } else { + handleStartStepHook(testCase, hook); } - } else if (event.getTestStep() instanceof HookTestStep) { - initHook((HookTestStep) event.getTestStep()); + } else if (event.getTestStep() instanceof PickleStepTestStep) { + handleStartPickleStep(testCase, (PickleStepTestStep) event.getTestStep()); } } - private void initHook(final HookTestStep hook) { + private void handleStartPickleStep(final TestCase testCase, + final PickleStepTestStep pickleStep) { + final String uuid = testCase.getId().toString(); + final Step step = pickleStep.getStep(); - final FixtureResult hookResult = new FixtureResult() + final StepResult stepResult = new StepResult() + .setName(step.getKeyword() + step.getText()) + .setStart(System.currentTimeMillis()); + + lifecycle.setCurrentTestCase(uuid); + lifecycle.startStep(uuid, pickleStep.getId().toString(), stepResult); + + final StepArgument stepArgument = step.getArgument(); + if (stepArgument instanceof DataTableArgument) { + final DataTableArgument dataTableArgument = (DataTableArgument) stepArgument; + createDataTableAttachment(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.setCurrentTestCase(uuid); + lifecycle.startStep(uuid, 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()); + + lifecycle.startTestContainer(new TestResultContainer() + .setUuid(containerUuid) + .setChildren(Collections.singletonList(uuid)) + ); + + final FixtureResult hookResult = new FixtureResult() + .setName(hook.getCodeLocation()); + + final String fixtureUuid = hookId.toString(); if (hook.getHookType() == HookType.BEFORE) { - lifecycle.startPrepareFixture(getTestContainerUuid(), getHookStepUuid(hook), hookResult); + lifecycle.startPrepareFixture(containerUuid, fixtureUuid, hookResult); } else { - lifecycle.startTearDownFixture(getTestContainerUuid(), getHookStepUuid(hook), hookResult); + lifecycle.startTearDownFixture(containerUuid, fixtureUuid, hookResult); } - } private void handleTestStepFinished(final TestStepFinished event) { if (event.getTestStep() instanceof HookTestStep) { - handleHookStep(event); - } else { - handlePickleStep(event); + 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) { lifecycle.addAttachment( "Text output", @@ -265,33 +305,16 @@ private void handleEmbedEvent(final EmbedEvent event) { lifecycle.addAttachment(event.name, event.getMediaType(), null, new ByteArrayInputStream(event.getData())); } - /* - Utility Methods - */ - - private String getTestContainerUuid() { - return currentContainer.get(); - } - - private String getTestCaseUuid(final TestCase testCase) { - return scenarioUuids.computeIfAbsent(getHistoryId(testCase), it -> UUID.randomUUID().toString()); - } - - private String getStepUuid(final PickleStepTestStep step) { - return currentFeature.get().getName() + getTestCaseUuid(currentTestCase.get()) - + step.getStep().getText() + step.getStep().getLine(); - } - - private String getHookStepUuid(final HookTestStep step) { - return currentFeature.get().getName() + getTestCaseUuid(currentTestCase.get()) - + step.getHookType().toString() + step.getCodeLocation(); - } - private String getHistoryId(final TestCase testCase) { - final String testCaseLocation = getTestCaseUri(testCase) + ":" + testCase.getLocation().getLine(); + final String testCaseLocation = getTestCaseUri(testCase) + COLON + testCase.getLocation().getLine(); return md5(testCaseLocation); } + 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)) { @@ -318,8 +341,9 @@ private Status translateTestCaseStatus(final Result testCaseResult) { } private List getExamplesAsParameters( - final Scenario scenario, final TestCase localCurrentTestCase - ) { + final Scenario scenario, + final TestCase localCurrentTestCase) { + final Optional maybeExample = scenario.getExamples().stream() .filter(example -> example.getTableBody().stream() @@ -343,10 +367,21 @@ private List getExamplesAsParameters( } 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, examples.getTableHeader().get().getCells().size()) + return IntStream.range(0, size) .mapToObj(index -> { - final String name = examples.getTableHeader().get().getCells().get(index).getValue(); + final String name = Objects.nonNull(headerNames) + ? headerNames.get(index) + : "arg" + index; final String value = row.getCells().get(index).getValue(); return createParameter(name, value); }) @@ -375,83 +410,56 @@ private void createDataTableAttachment(final DataTableArgument dataTableArgument new ByteArrayInputStream(dataTableCsv.toString().getBytes(StandardCharsets.UTF_8))); } - private void handleHookStep(final TestStepFinished event) { - final HookTestStep hookStep = (HookTestStep) event.getTestStep(); - final String uuid = getHookStepUuid(hookStep); - final FixtureResult fixtureResult = new FixtureResult().setStatus(translateTestCaseStatus(event.getResult())); - - if (!Status.PASSED.equals(fixtureResult.getStatus())) { - final TestResult testResult = new TestResult().setStatus(translateTestCaseStatus(event.getResult())); - final StatusDetails statusDetails = getStatusDetails(event.getResult().getError()) - .orElseGet(StatusDetails::new); - - final String errorMessage = event.getResult().getError() == null - ? hookStep.getHookType().name() + " is failed." - : hookStep.getHookType().name() - + " is failed: " - + event.getResult().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.getId()); + if (Objects.isNull(containerUuid)) { + // maybe throw an exception? + return; } - lifecycle.updateFixture(uuid, result -> result.setStatus(fixtureResult.getStatus()) - .setStatusDetails(fixtureResult.getStatusDetails())); + final String uuid = hook.getId().toString(); + + 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); - } - private void handlePickleStep(final TestStepFinished event) { + lifecycle.stopTestContainer(containerUuid); + lifecycle.writeTestContainer(containerUuid); + } - final Status stepStatus = translateTestCaseStatus(event.getResult()); - final StatusDetails statusDetails; - if (event.getResult().getStatus() == io.cucumber.plugin.event.Status.UNDEFINED) { - updateTestCaseStatus(Status.PASSED); + private void handleStopStep(final TestCase testCase, + final Result eventResult, + final UUID stepId) { + final Feature feature = testSources.getFeature(testCase.getUri()); - statusDetails = - getStatusDetails(new IllegalStateException("Undefined Step. Please add step definition")) - .orElse(new StatusDetails()); - lifecycle.updateTestCase(getTestCaseUuid(currentTestCase.get()), scenarioResult -> - scenarioResult - .setStatusDetails(statusDetails)); - } else { - statusDetails = - getStatusDetails(event.getResult().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() == io.cucumber.plugin.event.Status.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.getTestStep()), - stepResult -> stepResult.setStatus(stepStatus).setStatusDetails(statusDetails)); - lifecycle.stopStep(getStepUuid((PickleStepTestStep) event.getTestStep())); - } - - private void updateTestCaseStatus(final Status status) { - if (!forbidTestCaseStatusChange.get()) { - lifecycle.updateTestCase(getTestCaseUuid(currentTestCase.get()), - result -> result.setStatus(status)); - } + final String stepUuid = stepId.toString(); + lifecycle.updateStep( + stepUuid, + stepResult -> stepResult + .setStatus(stepStatus) + .setStatusDetails(statusDetails) + ); + lifecycle.stopStep(stepUuid); } } diff --git a/allure-cucumber7-jvm/src/test/java/io/qameta/allure/cucumber7jvm/AllureCucumber7JvmTest.java b/allure-cucumber7-jvm/src/test/java/io/qameta/allure/cucumber7jvm/AllureCucumber7JvmTest.java index ff644c18b..45da3d019 100644 --- a/allure-cucumber7-jvm/src/test/java/io/qameta/allure/cucumber7jvm/AllureCucumber7JvmTest.java +++ b/allure-cucumber7-jvm/src/test/java/io/qameta/allure/cucumber7jvm/AllureCucumber7JvmTest.java @@ -37,6 +37,7 @@ 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 io.qameta.allure.test.AllureFeatures; import io.qameta.allure.test.AllureResults; import io.qameta.allure.test.RunUtils; @@ -59,6 +60,7 @@ import static io.qameta.allure.util.ResultsUtils.PACKAGE_LABEL_NAME; import static io.qameta.allure.util.ResultsUtils.SUITE_LABEL_NAME; import static io.qameta.allure.util.ResultsUtils.TEST_CLASS_LABEL_NAME; +import static io.qameta.allure.util.ResultsUtils.md5; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; import static org.junit.jupiter.api.parallel.ResourceAccessMode.READ_WRITE; @@ -87,8 +89,10 @@ void shouldSetStatus() { final List testResults = results.getTestResults(); assertThat(testResults) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.PASSED); + .extracting(TestResult::getName, TestResult::getStatus) + .containsExactlyInAnyOrder( + tuple("Add a to b", Status.PASSED) + ); } @AllureFeatures.FailedTests @@ -282,10 +286,10 @@ void shouldAddBackgroundSteps() { .flatExtracting(TestResult::getSteps) .extracting(StepResult::getName) .containsExactly( - "Given cat is sad", - "And cat is murmur", - "When Pet the cat", - "Then Cat is happy" + "Given cat is sad", + "And cat is murmur", + "When Pet the cat", + "Then Cat is happy" ); } @@ -447,15 +451,17 @@ void shouldProcessUndefinedSteps() { final List testResults = results.getTestResults(); assertThat(testResults) - .extracting(TestResult::getStatus) - .containsExactlyInAnyOrder(Status.SKIPPED); + .extracting(TestResult::getName, TestResult::getStatus) + .containsExactlyInAnyOrder( + tuple("Step is not defined", null) + ); assertThat(testResults.get(0).getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 5", Status.PASSED), - tuple("When step is undefined", null), - tuple("Then b is 10", Status.SKIPPED) + tuple("Given a is 5", Status.PASSED), + tuple("When step is undefined", null), + tuple("Then b is 10", Status.SKIPPED) ); } @@ -473,9 +479,9 @@ void shouldProcessPendingExceptionsInSteps() { assertThat(testResults.get(0).getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 5", Status.PASSED), - tuple("When step is yet to be implemented", Status.SKIPPED), - tuple("Then b is 10", Status.SKIPPED) + tuple("Given a is 5", Status.PASSED), + tuple("When step is yet to be implemented", Status.SKIPPED), + tuple("Then b is 10", Status.SKIPPED) ); } @@ -494,10 +500,10 @@ void shouldSupportDryRunForSimpleFeatures() { assertThat(testResults.get(0).getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 5", Status.PASSED), - tuple("And b is 10", Status.PASSED), - tuple("When I add a to b", Status.PASSED), - tuple("Then result is 15", Status.PASSED) + tuple("Given a is 5", Status.PASSED), + tuple("And b is 10", Status.PASSED), + tuple("When I add a to b", Status.PASSED), + tuple("Then result is 15", Status.PASSED) ); } @@ -508,32 +514,29 @@ void shouldSupportDryRunForHooks() { final AllureResults results = runFeature("features/hooks.feature", "--dry-run", "-t", "@WithHooks or @BeforeHookWithException or @AfterHookWithException"); - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getName, TestResult::getStatus) - .startsWith( - tuple("Simple scenario with Before and After hooks", Status.PASSED) - ); + final TestResult tr1 = results.getTestResultByName("Simple scenario with Before and After hooks"); - assertThat(results.getTestResultContainers().get(0).getBefores()) + assertThat(results.getTestResultContainersForTestResult(tr1)) + .flatExtracting(TestResultContainer::getBefores) .extracting(FixtureResult::getName, FixtureResult::getStatus) .containsExactlyInAnyOrder( tuple("io.qameta.allure.cucumber7jvm.samples.HookSteps.beforeHook()", Status.PASSED) ); - assertThat(results.getTestResultContainers().get(0).getAfters()) + assertThat(results.getTestResultContainersForTestResult(tr1)) + .flatExtracting(TestResultContainer::getAfters) .extracting(FixtureResult::getName, FixtureResult::getStatus) .containsExactlyInAnyOrder( tuple("io.qameta.allure.cucumber7jvm.samples.HookSteps.afterHook()", Status.PASSED) ); - assertThat(testResults.get(0).getSteps()) + assertThat(tr1.getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 7", Status.PASSED), - tuple("And b is 8", Status.PASSED), - tuple("When I add a to b", Status.PASSED), - tuple("Then result is 15", Status.PASSED) + tuple("Given a is 7", Status.PASSED), + tuple("And b is 8", Status.PASSED), + tuple("When I add a to b", Status.PASSED), + tuple("Then result is 15", Status.PASSED) ); } @@ -568,6 +571,42 @@ void shouldPersistDifferentHistoryIdComparedToTheSameTestCaseInDifferentLocation .isNotEqualTo(results2.getTestResults().get(0).getHistoryId()); } + @AllureFeatures.History + @Test + void shouldSetTestCaseIdForScenarios() { + final AllureResults results = runFeature("features/simple.feature"); + + final List testResults = results.getTestResults(); + assertThat(testResults) + .extracting(TestResult::getName, TestResult::getTestCaseId) + .containsExactlyInAnyOrder( + tuple( + "Add a to b", + md5("src/test/resources/features/simple.feature:Add a to b") + ) + ); + } + + @AllureFeatures.History + @Test + void shouldSetTestCaseIdForExamples() { + final AllureResults results = runFeature("features/examples.feature", "--threads", "2"); + + final List testResults = results.getTestResults(); + assertThat(testResults) + .extracting(TestResult::getName, TestResult::getTestCaseId) + .containsExactlyInAnyOrder( + tuple( + "Scenario with Positive Examples", + md5("src/test/resources/features/examples.feature:Scenario with Positive Examples") + ), + tuple( + "Scenario with Positive Examples", + md5("src/test/resources/features/examples.feature:Scenario with Positive Examples") + ) + ); + } + @AllureFeatures.Parallel @Test void shouldProcessScenariosInParallelMode() { @@ -579,30 +618,33 @@ void shouldProcessScenariosInParallelMode() { .hasSize(3); assertThat(testResults) - .extracting(testResult -> testResult.getSteps().stream().map(StepResult::getName).collect(Collectors.toList())) + .flatExtracting(TestResult::getSteps) + .extracting(StepResult::getName) .containsSubsequence( - Arrays.asList("Given a is 1", - "And b is 3", - "When I add a to b", - "Then result is 4") + "Given a is 1", + "And b is 3", + "When I add a to b", + "Then result is 4" ); assertThat(testResults) - .extracting(testResult -> testResult.getSteps().stream().map(StepResult::getName).collect(Collectors.toList())) + .flatExtracting(TestResult::getSteps) + .extracting(StepResult::getName) .containsSubsequence( - Arrays.asList("Given a is 2", - "And b is 4", - "When I add a to b", - "Then result is 6") + "Given a is 2", + "And b is 4", + "When I add a to b", + "Then result is 6" ); assertThat(testResults) - .extracting(testResult -> testResult.getSteps().stream().map(StepResult::getName).collect(Collectors.toList())) + .flatExtracting(TestResult::getSteps) + .extracting(StepResult::getName) .containsSubsequence( - Arrays.asList("Given a is 7", - "And b is 8", - "When I add a to b", - "Then result is 15") + "Given a is 7", + "And b is 8", + "When I add a to b", + "Then result is 15" ); } @@ -613,64 +655,61 @@ void shouldDisplayHooksAsStages() { final AllureResults results = runFeature("features/hooks.feature", "-t", "@WithHooks or @BeforeHookWithException or @AfterHookWithException"); - final List testResults = results.getTestResults(); - assertThat(testResults) - .extracting(TestResult::getName, TestResult::getStatus) - .containsExactlyInAnyOrder( - tuple("Simple scenario with Before and After hooks", Status.PASSED), - tuple("Simple scenario with Before hook with Exception", Status.SKIPPED), - tuple("Simple scenario with After hook with Exception", Status.BROKEN) - ); + final TestResult tr1 = results.getTestResultByName("Simple scenario with Before and After hooks"); + final TestResult tr2 = results.getTestResultByName("Simple scenario with Before hook with Exception"); + final TestResult tr3 = results.getTestResultByName("Simple scenario with After hook with Exception"); - assertThat(testResults.get(0).getSteps()) + assertThat(tr1.getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 7", Status.PASSED), - tuple("And b is 8", Status.PASSED), - tuple("When I add a to b", Status.PASSED), - tuple("Then result is 15", Status.PASSED) + tuple("Given a is 7", Status.PASSED), + tuple("And b is 8", Status.PASSED), + tuple("When I add a to b", Status.PASSED), + tuple("Then result is 15", Status.PASSED) ); - - assertThat(results.getTestResultContainers().get(0).getBefores()) + assertThat(results.getTestResultContainersForTestResult(tr1)) + .flatExtracting(TestResultContainer::getBefores) .extracting(FixtureResult::getName, FixtureResult::getStatus) .containsExactlyInAnyOrder( tuple("io.qameta.allure.cucumber7jvm.samples.HookSteps.beforeHook()", Status.PASSED) ); - assertThat(results.getTestResultContainers().get(0).getAfters()) + assertThat(results.getTestResultContainersForTestResult(tr1)) + .flatExtracting(TestResultContainer::getAfters) .extracting(FixtureResult::getName, FixtureResult::getStatus) .containsExactlyInAnyOrder( tuple("io.qameta.allure.cucumber7jvm.samples.HookSteps.afterHook()", Status.PASSED) ); - assertThat(testResults.get(1).getSteps()) + assertThat(tr2.getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 7", Status.SKIPPED), - tuple("And b is 8", Status.SKIPPED), - tuple("When I add a to b", Status.SKIPPED), - tuple("Then result is 15", Status.SKIPPED) + tuple("Given a is 7", Status.SKIPPED), + tuple("And b is 8", Status.SKIPPED), + tuple("When I add a to b", Status.SKIPPED), + tuple("Then result is 15", Status.SKIPPED) ); - assertThat(results.getTestResultContainers().get(1).getBefores()) + assertThat(results.getTestResultContainersForTestResult(tr2)) + .flatExtracting(TestResultContainer::getBefores) .extracting(FixtureResult::getName, FixtureResult::getStatus) .containsExactlyInAnyOrder( tuple("io.qameta.allure.cucumber7jvm.samples.HookSteps.beforeHookWithException()", Status.FAILED) ); - - assertThat(testResults.get(2).getSteps()) + assertThat(tr3.getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactlyInAnyOrder( - tuple("Given a is 7", Status.PASSED), - tuple("And b is 8", Status.PASSED), - tuple("When I add a to b", Status.PASSED), - tuple("Then result is 15", Status.PASSED) + tuple("Given a is 7", Status.PASSED), + tuple("And b is 8", Status.PASSED), + tuple("When I add a to b", Status.PASSED), + tuple("Then result is 15", Status.PASSED) ); - assertThat(results.getTestResultContainers().get(2).getAfters()) + assertThat(results.getTestResultContainersForTestResult(tr3)) + .flatExtracting(TestResultContainer::getAfters) .extracting(FixtureResult::getName, FixtureResult::getStatus) .containsExactlyInAnyOrder( tuple("io.qameta.allure.cucumber7jvm.samples.HookSteps.afterHookWithException()", Status.FAILED) @@ -687,14 +726,14 @@ void shouldHandleAmbigiousStepsExceptions() { assertThat(testResults) .extracting(TestResult::getName, TestResult::getStatus) .containsExactlyInAnyOrder( - tuple("Simple scenario with ambigious steps", Status.SKIPPED) + tuple("Simple scenario with ambigious steps", null) ); assertThat(testResults.get(0).getSteps()) .extracting(StepResult::getName, StepResult::getStatus) .containsExactly( - tuple("When ambigious step present", null), - tuple("Then something bad should happen", Status.SKIPPED) + tuple("When ambigious step present", null), + tuple("Then something bad should happen", Status.SKIPPED) ); } @@ -714,6 +753,36 @@ void shouldSupportProvidedLabels() { ); } + @Test + void shouldSupportRuntimeApiInStepsWhenHooksAreUsed() { + final AllureResults results = runFeature("features/runtimeapi.feature"); + + final List testResults = results.getTestResults(); + + assertThat(testResults) + .hasSize(1) + .flatExtracting(TestResult::getSteps) + .extracting(StepResult::getName) + .containsExactly( + "When step 1", + "When step 2", + "And step 3", + "Then step 4", + "And step 5" + ); + + assertThat(testResults) + .flatExtracting(TestResult::getLinks) + .extracting(Link::getName, Link::getUrl) + .containsExactly( + tuple("step1", "https://example.org/step1"), + tuple("step2", "https://example.org/step2"), + tuple("step3", "https://example.org/step3"), + tuple("step4", "https://example.org/step4"), + tuple("step5", "https://example.org/step5") + ); + } + @SystemProperty(name = "cucumber.junit-platform.naming-strategy", value = "long") @Step private AllureResults runFeature(final String featureResource, diff --git a/allure-cucumber7-jvm/src/test/java/io/qameta/allure/cucumber7jvm/samples/RuntimeApiSteps.java b/allure-cucumber7-jvm/src/test/java/io/qameta/allure/cucumber7jvm/samples/RuntimeApiSteps.java new file mode 100644 index 000000000..0f66fa87c --- /dev/null +++ b/allure-cucumber7-jvm/src/test/java/io/qameta/allure/cucumber7jvm/samples/RuntimeApiSteps.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.cucumber7jvm.samples; + +import io.cucumber.java.Before; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import io.cucumber.java.en.And; +import io.qameta.allure.Allure; + +/** + * @author charlie (Dmitry Baev). + */ +public class RuntimeApiSteps { + + @Before("@beforeScenario") + public void beforeScenario(){ + // nothing + } + + @Before("@beforeFeature") + public void beforeFeature(){ + // nothing + } + + @When("^step 1$") + public void step1() { + Allure.step("step1 nested"); + Allure.link("step1", "https://example.org/step1"); + Allure.getLifecycle().getCurrentTestCase().ifPresent(uuid -> { + System.out.println("step1: " + uuid); + }); + } + + @When("^step 2$") + public void step2() { + Allure.step("step2 nested"); + Allure.link("step2", "https://example.org/step2"); + Allure.getLifecycle().getCurrentTestCase().ifPresent(uuid -> { + System.out.println("step2: " + uuid); + }); + } + + @And("^step 3$") + public void step3() { + Allure.step("step3 nested"); + Allure.link("step3", "https://example.org/step3"); + Allure.getLifecycle().getCurrentTestCase().ifPresent(uuid -> { + System.out.println("step3: " + uuid); + }); + } + + @Then("^step 4$") + public void step4() { + Allure.step("step4 nested"); + Allure.link("step4", "https://example.org/step4"); + Allure.getLifecycle().getCurrentTestCase().ifPresent(uuid -> { + System.out.println("step4: " + uuid); + }); + } + + @And("^step 5$") + public void step5() { + Allure.step("step5 nested"); + Allure.link("step5", "https://example.org/step5"); + Allure.getLifecycle().getCurrentTestCase().ifPresent(uuid -> { + System.out.println("step5: " + uuid); + }); + } +} diff --git a/allure-cucumber7-jvm/src/test/resources/features/runtimeapi.feature b/allure-cucumber7-jvm/src/test/resources/features/runtimeapi.feature new file mode 100644 index 000000000..ec43eccb7 --- /dev/null +++ b/allure-cucumber7-jvm/src/test/resources/features/runtimeapi.feature @@ -0,0 +1,10 @@ +@beforeFeature +Feature: Should support runtime API in all steps + + @beforeScenario + Scenario: Scenario with Runtime API usage + When step 1 + When step 2 + And step 3 + Then step 4 + And step 5 diff --git a/allure-java-commons-test/src/main/java/io/qameta/allure/test/AllureResults.java b/allure-java-commons-test/src/main/java/io/qameta/allure/test/AllureResults.java index 621b213ae..a836a04d7 100644 --- a/allure-java-commons-test/src/main/java/io/qameta/allure/test/AllureResults.java +++ b/allure-java-commons-test/src/main/java/io/qameta/allure/test/AllureResults.java @@ -20,6 +20,9 @@ import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.stream.Collectors; /** * @author charlie (Dmitry Baev). @@ -32,4 +35,20 @@ public interface AllureResults { Map getAttachments(); + default TestResult getTestResultByName(final String name) { + return getTestResults().stream() + .filter(tr -> Objects.equals(name, tr.getName())) + .findFirst() + .orElseThrow(() -> new NoSuchElementException( + "test result with name " + name + " is not found" + )); + } + + default List getTestResultContainersForTestResult(final TestResult testResult) { + return getTestResultContainers().stream() + .filter(c -> Objects.nonNull(c.getChildren())) + .filter(c -> c.getChildren().contains(testResult.getUuid())) + .collect(Collectors.toList()); + } + } diff --git a/allure-java-commons-test/src/main/java/io/qameta/allure/test/AllureTestCommonsUtils.java b/allure-java-commons-test/src/main/java/io/qameta/allure/test/AllureTestCommonsUtils.java index ae099c33f..e0e65881b 100644 --- a/allure-java-commons-test/src/main/java/io/qameta/allure/test/AllureTestCommonsUtils.java +++ b/allure-java-commons-test/src/main/java/io/qameta/allure/test/AllureTestCommonsUtils.java @@ -15,16 +15,25 @@ */ package io.qameta.allure.test; +import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; import io.qameta.allure.Allure; import io.qameta.allure.AllureConstants; +import io.qameta.allure.model.Parameter; +import io.qameta.allure.model.Stage; +import io.qameta.allure.model.Status; import java.io.ByteArrayInputStream; +import java.io.IOException; import java.io.UncheckedIOException; +import java.util.Locale; -import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_DEFAULT; import static com.fasterxml.jackson.databind.MapperFeature.USE_WRAPPER_NAME_AS_PROPERTY_NAME; /** @@ -35,8 +44,13 @@ public final class AllureTestCommonsUtils { private static final ObjectWriter WRITER = JsonMapper .builder() .configure(USE_WRAPPER_NAME_AS_PROPERTY_NAME, true) - .serializationInclusion(NON_NULL) + .serializationInclusion(NON_DEFAULT) .build() + .registerModule(new SimpleModule() + .addSerializer(Status.class, new StatusSerializer()) + .addSerializer(Stage.class, new StageSerializer()) + .addSerializer(Parameter.Mode.class, new ParameterModeSerializer()) + ) .writerWithDefaultPrettyPrinter(); private AllureTestCommonsUtils() { @@ -77,4 +91,52 @@ public static void attach(final AllureResults allureResults) { ); } + /** + * Parameter mode serializer. + */ + private static class ParameterModeSerializer extends StdSerializer { + protected ParameterModeSerializer() { + super(Parameter.Mode.class); + } + + @Override + public void serialize(final Parameter.Mode value, + final JsonGenerator gen, + final SerializerProvider provider) throws IOException { + gen.writeString(value.name().toLowerCase(Locale.ENGLISH)); + } + } + + /** + * Stage serializer. + */ + private static class StageSerializer extends StdSerializer { + protected StageSerializer() { + super(Stage.class); + } + + @Override + public void serialize(final Stage value, + final JsonGenerator gen, + final SerializerProvider provider) throws IOException { + gen.writeString(value.name().toLowerCase(Locale.ENGLISH)); + } + } + + /** + * Status serializer. + */ + private static class StatusSerializer extends StdSerializer { + protected StatusSerializer() { + super(Status.class); + } + + @Override + public void serialize(final Status value, + final JsonGenerator gen, + final SerializerProvider provider) throws IOException { + gen.writeString(value.name().toLowerCase(Locale.ENGLISH)); + } + } + } diff --git a/allure-java-commons/src/test/java/io/qameta/allure/AllureLifecycleTest.java b/allure-java-commons/src/test/java/io/qameta/allure/AllureLifecycleTest.java index 02a74faa8..0a06dd32b 100644 --- a/allure-java-commons/src/test/java/io/qameta/allure/AllureLifecycleTest.java +++ b/allure-java-commons/src/test/java/io/qameta/allure/AllureLifecycleTest.java @@ -35,6 +35,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; @@ -70,6 +71,23 @@ public void setUp() { lifecycle = new AllureLifecycle(writer); } + @Test + void shouldReturnCurrentTestCaseId() { + final String uuid = randomId(); + final String name = randomName(); + final TestResult result = new TestResult().setUuid(uuid).setName(name); + lifecycle.scheduleTestCase(result); + lifecycle.startTestCase(uuid); + + final String stepUuid = randomId(); + lifecycle.startStep(uuid, stepUuid, new StepResult().setName(randomName())); + + final Optional currentTestCase = lifecycle.getCurrentTestCase(); + assertThat(currentTestCase) + .isPresent() + .hasValue(uuid); + } + @Test void shouldCreateTest() { final String uuid = randomId();