From f9c6b2ad2748f705a692aab7153c5fe190f5b45c Mon Sep 17 00:00:00 2001 From: Dmitry Baev Date: Fri, 10 Jul 2026 21:18:53 +0100 Subject: [PATCH] fix history id calculation --- .../io/qameta/allure/citrus/AllureCitrus.java | 23 ++- .../allure/citrus/AllureCitrusTest.java | 57 +++++++ .../cucumber7jvm/AllureCucumber7Jvm.java | 16 +- .../cucumber7jvm/AllureCucumber7JvmTest.java | 42 ++++- .../cucumber7jvm/samples/RuntimeApiSteps.java | 2 + .../allure/test/AllureTestCommonsUtils.java | 30 ++++ .../io/qameta/allure/AllureLifecycle.java | 34 +++- .../io/qameta/allure/AllureLifecycleTest.java | 150 ++++++++++++++++++ .../allure/jbehave5/AllureJbehave5.java | 32 +--- .../allure/jbehave5/AllureJbehave5Test.java | 13 +- .../jbehave5/samples/RuntimeApiSteps.java | 1 + .../junitplatform/AllureJunitPlatform.java | 25 +-- .../AllureJunitPlatformTest.java | 60 ++++++- .../features/RuntimeParametersTest.java | 28 ++++ .../io/qameta/allure/junit4/AllureJunit4.java | 19 ++- .../allure/junit4/AllureJunit4Test.java | 44 ++++- .../junit4/samples/RuntimeParametersTest.java | 28 ++++ .../allure/jupiter/AllureJupiterTest.java | 40 +++++ .../features/ParameterizedClassTests.java | 3 + .../io/qameta/allure/karate/AllureKarate.java | 13 +- .../allure/karate/AllureKarateTest.java | 41 ++++- .../resources/testdata/runtime-api.feature | 10 ++ .../allure/scalatest/AllureScalatest.scala | 10 +- .../scalatest/AllureScalatestTest.scala | 40 +++++ .../scalatest/testdata/PendingSpec.scala | 28 ++++ .../testdata/RuntimeParameterSpec.scala | 32 ++++ .../io/qameta/allure/spock2/AllureSpock2.java | 19 --- .../allure/spock2/AllureSpock2Test.java | 41 ++++- .../samples/RuntimeParameterTest.groovy | 33 ++++ .../io/qameta/allure/testng/AllureTestNg.java | 45 ++---- .../allure/testng/AllureTestNgTest.java | 55 ++++++- .../testng/samples/RuntimeParametersTest.java | 28 ++++ .../testng/samples/SuccessPercentageTest.java | 42 +++++ 33 files changed, 934 insertions(+), 150 deletions(-) create mode 100644 allure-junit-platform/src/test/java/io/qameta/allure/junitplatform/features/RuntimeParametersTest.java create mode 100644 allure-junit4/src/test/java/io/qameta/allure/junit4/samples/RuntimeParametersTest.java create mode 100644 allure-karate/src/test/resources/testdata/runtime-api.feature create mode 100644 allure-scalatest/src/test/scala/io/qameta/allure/scalatest/testdata/PendingSpec.scala create mode 100644 allure-scalatest/src/test/scala/io/qameta/allure/scalatest/testdata/RuntimeParameterSpec.scala create mode 100644 allure-spock2/src/test/groovy/io/qameta/allure/spock2/samples/RuntimeParameterTest.groovy create mode 100644 allure-testng/src/test/java/io/qameta/allure/testng/samples/RuntimeParametersTest.java create mode 100644 allure-testng/src/test/java/io/qameta/allure/testng/samples/SuccessPercentageTest.java diff --git a/allure-citrus/src/main/java/io/qameta/allure/citrus/AllureCitrus.java b/allure-citrus/src/main/java/io/qameta/allure/citrus/AllureCitrus.java index 5237974bb..b9d7f93b6 100644 --- a/allure-citrus/src/main/java/io/qameta/allure/citrus/AllureCitrus.java +++ b/allure-citrus/src/main/java/io/qameta/allure/citrus/AllureCitrus.java @@ -60,6 +60,7 @@ import static io.qameta.allure.util.ResultsUtils.createThreadLabel; import static io.qameta.allure.util.ResultsUtils.createTitlePath; import static io.qameta.allure.util.ResultsUtils.getProvidedLabels; +import static io.qameta.allure.util.ResultsUtils.md5; /** * Reports Citrus test execution to Allure. @@ -187,7 +188,10 @@ public void onTestFailure(final TestCase test, final Throwable cause) { */ @Override public void onTestSkipped(final TestCase test) { - //do nothing + if (!isTestStarted(test)) { + startTest(test); + } + stopTest(test, Status.SKIPPED, null); } /** @@ -217,9 +221,15 @@ public void onTestActionSkipped(final TestCase testCase, final TestAction testAc private void startTest(final TestCase testCase) { final AllureExternalKey testKey = createTestKey(testCase); final Optional> testClass = Optional.ofNullable(testCase.getTestClass()); + final String fullName = testClass + .map(Class::getName) + .map(className -> className + "." + testCase.getName()) + .orElseGet(testCase::getName); final TestResult result = new TestResult() .setName(testCase.getName()) + .setFullName(fullName) + .setTestCaseId(md5(fullName)) .setTitlePath( testClass .map(ResultsUtils::createTitlePathFromJavaClass) @@ -267,7 +277,7 @@ private void stopTest(final TestCase testCase, .collect(Collectors.toList()); getLifecycle().updateTest(testKey, result -> { - result.setParameters(parameters); + result.getParameters().addAll(parameters); result.setStatus(status); result.setStatusDetails(details); }); @@ -286,6 +296,15 @@ private AllureExternalKey createTestKey(final TestCase testCase) { return testKey; } + private boolean isTestStarted(final TestCase testCase) { + try { + lock.readLock().lock(); + return testKeys.containsKey(testCase); + } finally { + lock.readLock().unlock(); + } + } + private AllureExternalKey removeTestKey(final TestCase testCase) { try { lock.writeLock().lock(); diff --git a/allure-citrus/src/test/java/io/qameta/allure/citrus/AllureCitrusTest.java b/allure-citrus/src/test/java/io/qameta/allure/citrus/AllureCitrusTest.java index e7d400720..6f2f33da5 100644 --- a/allure-citrus/src/test/java/io/qameta/allure/citrus/AllureCitrusTest.java +++ b/allure-citrus/src/test/java/io/qameta/allure/citrus/AllureCitrusTest.java @@ -18,6 +18,7 @@ import com.consol.citrus.Citrus; import com.consol.citrus.CitrusContext; import com.consol.citrus.TestCase; +import com.consol.citrus.TestCaseMetaInfo; import com.consol.citrus.actions.AbstractTestAction; import com.consol.citrus.actions.FailAction; import com.consol.citrus.context.TestContext; @@ -27,6 +28,7 @@ import io.qameta.allure.AllureLifecycle; import io.qameta.allure.Step; import io.qameta.allure.model.Parameter; +import io.qameta.allure.model.Stage; import io.qameta.allure.model.Status; import io.qameta.allure.model.StatusDetails; import io.qameta.allure.model.StepResult; @@ -40,6 +42,7 @@ import java.time.Instant; +import static io.qameta.allure.util.ResultsUtils.md5; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; @SuppressWarnings("unchecked") @@ -203,6 +206,60 @@ void shouldSetParameters() { ); } + @AllureFeatures.History + @AllureFeatures.Parameters + @Test + void shouldCalculateIdsFromFinalNativeAndRuntimeParameters() { + final DefaultTestDesigner designer = new DefaultTestDesigner(); + designer.name("Runtime parameters"); + designer.variable("native", "example"); + designer.action(new AbstractTestAction() { + @Override + public void doExecute(final TestContext context) { + Allure.parameter("runtime", "value"); + Allure.parameter("excluded", "ignored", true); + } + }); + + final AllureResults results = run(designer); + final TestResult testResult = results.getTestResults().get(0); + assertThat(testResult.getParameters()) + .extracting(Parameter::getName, Parameter::getValue, Parameter::getExcluded) + .containsExactlyInAnyOrder( + tuple("native", "example", null), + tuple("runtime", "value", null), + tuple("excluded", "ignored", true) + ); + + final String fullName = DefaultTestDesigner.class.getName() + ".Runtime parameters"; + assertThat(testResult.getTestCaseId()) + .isEqualTo(md5(fullName)); + assertThat(testResult.getHistoryId()) + .isEqualTo(md5(md5(fullName) + "native" + "example" + "runtime" + "value")); + } + + @AllureFeatures.SkippedTests + @AllureFeatures.History + @Test + void shouldReportDisabledTestsWithIds() { + final DefaultTestDesigner designer = new DefaultTestDesigner(); + designer.name("Disabled test"); + designer.variable("native", "value"); + designer.status(TestCaseMetaInfo.Status.DISABLED); + + final AllureResults results = run(designer); + final String fullName = DefaultTestDesigner.class.getName() + ".Disabled test"; + final String testCaseId = md5(fullName); + assertThat(results.getTestResults()) + .singleElement() + .satisfies(result -> { + assertThat(result.getStatus()).isEqualTo(Status.SKIPPED); + assertThat(result.getStage()).isEqualTo(Stage.FINISHED); + assertThat(result.getTestCaseId()).isEqualTo(testCaseId); + assertThat(result.getHistoryId()).isEqualTo(md5(testCaseId + "native" + "value")); + }); + } + @Step("Run test case {testDesigner}") private AllureResults run(final TestDesigner testDesigner) { // a failing citrus test is a valid outcome under test — only fail the harness when 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 6b7e98795..ab84c24cf 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 @@ -178,11 +178,7 @@ private void handleTestCaseStarted(final TestCaseStarted event) { // 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.getLocation().getLine() - ); + final String fullName = getTestCaseLocation(testCase); final String testCaseUuid = testCase.getId().toString(); @@ -192,7 +188,6 @@ private void handleTestCaseStarted(final TestCaseStarted event) { final TestResult result = new TestResult() .setUuid(testCaseUuid) .setTestCaseId(getTestCaseId(testCase)) - .setHistoryId(getHistoryId(testCase)) .setFullName(fullName) .setTitlePath(titlePath) .setName(name) @@ -205,9 +200,7 @@ private void handleTestCaseStarted(final TestCaseStarted event) { ); if (scenarioDefinition.getExamples() != null) { - result.setParameters( - getExamplesAsParameters(scenarioDefinition, testCase) - ); + result.getParameters().addAll(getExamplesAsParameters(scenarioDefinition, testCase)); } final String description = Stream.of(feature.getDescription(), scenarioDefinition.getDescription()) @@ -364,9 +357,8 @@ private void handleEmbedEvent(final EmbedEvent event) { ); } - private String getHistoryId(final TestCase testCase) { - final String testCaseLocation = getTestCaseUri(testCase) + COLON + testCase.getLocation().getLine(); - return md5(testCaseLocation); + private String getTestCaseLocation(final TestCase testCase) { + return getTestCaseUri(testCase) + COLON + testCase.getLocation().getLine(); } private String getTestCaseId(final TestCase testCase) { 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 5c32a7f7f..6a2f72095 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 @@ -539,7 +539,7 @@ void shouldPersistHistoryIdForScenarios() { final List testResults = results.getTestResults(); assertThat(testResults.get(0).getHistoryId()) - .isEqualTo("892e5eabe51184301cf1358453c9f052"); + .isEqualTo(md5(md5("src/test/resources/features/simple.feature:Add a to b"))); } @AllureFeatures.History @@ -550,7 +550,16 @@ void shouldPersistHistoryIdForExamples() { final List testResults = results.getTestResults(); assertThat(testResults) .extracting(TestResult::getHistoryId) - .containsExactlyInAnyOrder("c0f824814a130048e9f86358363cf23e", "646aca5d0775cd4f13161e1ea1a68c39"); + .containsExactlyInAnyOrder( + md5( + md5("src/test/resources/features/examples.feature:Scenario with Positive Examples") + + "a1b3result4" + ), + md5( + md5("src/test/resources/features/examples.feature:Scenario with Positive Examples") + + "a2b4result6" + ) + ); } @AllureFeatures.History @@ -782,6 +791,35 @@ void shouldSupportRuntimeApiInStepsWhenHooksAreUsed() { ); } + @AllureFeatures.History + @AllureFeatures.Parameters + @Test + void shouldCalculateIdsFromRuntimeParametersAtTestEnd() { + final AllureResults results = runFeature("features/runtimeapi.feature"); + + final TestResult testResult = results.getTestResults().get(0); + assertThat(testResult.getParameters()) + .extracting(Parameter::getName, Parameter::getValue, Parameter::getExcluded) + .containsExactlyInAnyOrder( + tuple("runtime", "value", null), + tuple("excluded", "ignored", true) + ); + assertThat(testResult.getTestCaseId()) + .isEqualTo( + md5( + "src/test/resources/features/runtimeapi.feature:Scenario with Runtime API usage" + ) + ); + assertThat(testResult.getHistoryId()) + .isEqualTo( + md5( + md5( + "src/test/resources/features/runtimeapi.feature:Scenario with Runtime API usage" + ) + "runtimevalue" + ) + ); + } + @SystemProperty( name = "cucumber.junit-platform.naming-strategy", value = "long" 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 index a0535b302..6a4d35c6e 100644 --- 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 @@ -34,6 +34,8 @@ public void beforeFeature() { @When("^step 1$") public void step1() { + Allure.parameter("runtime", "value"); + Allure.parameter("excluded", "ignored", true); Allure.step("step1 nested"); Allure.link("step1", "https://example.org/step1"); Allure.getLifecycle().getCurrentRootKey().ifPresent(key -> { 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 70a4b8c3c..0f6389dc4 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 @@ -32,10 +32,15 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.UncheckedIOException; +import java.util.Comparator; +import java.util.List; import java.util.Locale; +import java.util.Objects; +import java.util.stream.Stream; import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_DEFAULT; import static com.fasterxml.jackson.databind.MapperFeature.USE_WRAPPER_NAME_AS_PROPERTY_NAME; +import static io.qameta.allure.util.ResultsUtils.md5; /** * Provides utility methods for Allure Java test support support. @@ -105,6 +110,31 @@ public static void attach(final AllureResults allureResults) { ); } + /** + * Calculates the compatibility history id expected in adapter tests. + * + * @param testCaseId the test case id + * @param parameters the final parameters + * @return the expected history id + */ + public static String expectedHistoryId(final String testCaseId, final List parameters) { + final StringBuilder source = new StringBuilder(testCaseId); + final Stream parameterStream = Objects.isNull(parameters) ? Stream.empty() : parameters.stream(); + parameterStream + .filter(Objects::nonNull) + .filter(parameter -> !Boolean.TRUE.equals(parameter.getExcluded())) + .sorted( + Comparator.comparing((Parameter parameter) -> Objects.toString(parameter.getName(), "")) + .thenComparing(parameter -> Objects.toString(parameter.getValue(), "")) + ) + .forEachOrdered( + parameter -> source + .append(Objects.toString(parameter.getName(), "")) + .append(Objects.toString(parameter.getValue(), "")) + ); + return md5(source.toString()); + } + private static AttachmentOptions attachmentOptions(final String fileName) { if (fileName.endsWith(DOT + JSON_EXTENSION) || fileName.endsWith(DOT + TEXT_EXTENSION)) { return AttachmentOptions.empty(); diff --git a/allure-java-commons/src/main/java/io/qameta/allure/AllureLifecycle.java b/allure-java-commons/src/main/java/io/qameta/allure/AllureLifecycle.java index 7c2ec119d..6c0473143 100644 --- a/allure-java-commons/src/main/java/io/qameta/allure/AllureLifecycle.java +++ b/allure-java-commons/src/main/java/io/qameta/allure/AllureLifecycle.java @@ -24,6 +24,7 @@ import io.qameta.allure.listener.TestLifecycleListener; import io.qameta.allure.model.Attachment; import io.qameta.allure.model.FixtureResult; +import io.qameta.allure.model.Parameter; import io.qameta.allure.model.ScopeFixtureResult; import io.qameta.allure.model.ScopeFixtureType; import io.qameta.allure.model.ScopeResult; @@ -45,6 +46,7 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -59,11 +61,13 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.stream.Stream; import static io.qameta.allure.AllureConstants.ATTACHMENT_FILE_SUFFIX; import static io.qameta.allure.util.ResultsUtils.firstNonEmpty; import static io.qameta.allure.util.ResultsUtils.getStatus; import static io.qameta.allure.util.ResultsUtils.getStatusDetails; +import static io.qameta.allure.util.ResultsUtils.md5; import static io.qameta.allure.util.ServiceLoaderUtils.load; /** @@ -325,8 +329,10 @@ public void updateTest(final Consumer update) { } /** - * Stops test by given key. The test must be running; scope metadata is merged into the test here. Unbinds the - * calling thread only if the test is the calling thread's root. + * Stops test by given key. The test must be running; scope metadata is merged into the test here. If the test has + * a test case id but no history id, a compatibility history id is generated from the test case id and the final + * parameters. A history id supplied by a {@link TestLifecycleListener#beforeTestStop(TestResult)} listener is + * preserved. Unbinds the calling thread only if the test is the calling thread's root. * * @param key the external test key */ @@ -347,13 +353,37 @@ public void stopTest(final AllureExternalKey key) { testResult .setStage(Stage.FINISHED) .setStop(System.currentTimeMillis()); + if (Objects.isNull(testResult.getParameters())) { + testResult.setParameters(new ArrayList<>()); + } applyScopeMetadata(item); + if (Objects.isNull(testResult.getHistoryId()) && Objects.nonNull(testResult.getTestCaseId())) { + testResult.setHistoryId(calculateHistoryId(testResult.getTestCaseId(), testResult.getParameters())); + } if (isCurrentRoot(key)) { threadContext.clear(); } notifier.afterTestStop(testResult); } + private static String calculateHistoryId(final String testCaseId, final List parameters) { + final StringBuilder source = new StringBuilder(testCaseId); + final Stream parameterStream = Objects.isNull(parameters) ? Stream.empty() : parameters.stream(); + parameterStream + .filter(Objects::nonNull) + .filter(parameter -> !Boolean.TRUE.equals(parameter.getExcluded())) + .sorted( + Comparator.comparing((Parameter parameter) -> Objects.toString(parameter.getName(), "")) + .thenComparing(parameter -> Objects.toString(parameter.getValue(), "")) + ) + .forEachOrdered( + parameter -> source + .append(Objects.toString(parameter.getName(), "")) + .append(Objects.toString(parameter.getValue(), "")) + ); + return md5(source.toString()); + } + /** * Writes test by given key. Waits for the test's pending async attachments before serializing, so the written * result file is a completion marker: everything it references exists. 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 a64442380..9e8ec871d 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 @@ -15,11 +15,14 @@ */ package io.qameta.allure; +import io.qameta.allure.listener.LifecycleNotifier; +import io.qameta.allure.listener.TestLifecycleListener; import io.qameta.allure.model.Attachment; import io.qameta.allure.model.FixtureResult; import io.qameta.allure.model.Label; import io.qameta.allure.model.Link; import io.qameta.allure.model.Parameter; +import io.qameta.allure.model.Stage; import io.qameta.allure.model.StepResult; import io.qameta.allure.model.TestResult; import io.qameta.allure.model.TestResultContainer; @@ -27,6 +30,7 @@ import io.qameta.allure.test.AllureResults; import io.qameta.allure.test.IsolatedLifecycle; import io.qameta.allure.test.RunUtils; +import io.qameta.allure.util.ResultsUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -191,6 +195,152 @@ void shouldMergeScopeMetadataIntoLiveTestsWhenScopeIsWrittenFirst() { .containsExactly(tuple("layer", "rest")); } + @Test + void shouldCalculateHistoryIdAfterScopeMergeForAfterStopListeners() { + final String testCaseId = randomId(); + final Parameter scopeParameter = new Parameter().setName("first").setValue(randomString(10)); + final Parameter runtimeParameter = new Parameter().setName("second").setValue(randomString(10)); + final Parameter excludedParameter = new Parameter() + .setName("excluded") + .setValue(randomString(10)) + .setExcluded(true); + final String expectedHistoryId = ResultsUtils.md5( + testCaseId + + scopeParameter.getName() + scopeParameter.getValue() + + runtimeParameter.getName() + runtimeParameter.getValue() + ); + final List historyIdsBeforeStop = new ArrayList<>(); + final List resultsAfterStop = new ArrayList<>(); + final TestLifecycleListener listener = new TestLifecycleListener() { + @Override + public void beforeTestStop(final TestResult result) { + historyIdsBeforeStop.add(result.getHistoryId()); + } + + @Override + public void afterTestStop(final TestResult result) { + resultsAfterStop.add(result); + } + }; + lifecycle = new AllureLifecycle( + writer, + new LifecycleNotifier(List.of(), List.of(listener), List.of(), List.of()) + ); + + final AllureExternalKey scopeKey = scopeKey(randomId()); + final AllureExternalKey fixtureKey = fixtureKey(randomId()); + final AllureExternalKey testKey = testKey(randomId()); + lifecycle.registerScope(scopeKey); + lifecycle.startBeforeFixture(scopeKey, fixtureKey, new FixtureResult().setName(randomName())); + lifecycle.updateTestMetadata(metadata -> metadata.getParameters().add(scopeParameter)); + lifecycle.stopFixture(fixtureKey); + + lifecycle.scheduleTest( + List.of(scopeKey), + testKey, + new TestResult().setName(randomName()).setTestCaseId(testCaseId) + ); + lifecycle.startTest(testKey); + lifecycle.updateTest( + testKey, result -> result.getParameters().addAll( + List.of(runtimeParameter, excludedParameter) + ) + ); + lifecycle.stopTest(testKey); + + assertThat(historyIdsBeforeStop) + .hasSize(1) + .first() + .isNull(); + assertThat(resultsAfterStop) + .singleElement() + .satisfies(result -> { + assertThat(result.getStage()).isEqualTo(Stage.FINISHED); + assertThat(result.getParameters()) + .containsExactly(runtimeParameter, excludedParameter, scopeParameter); + assertThat(result.getHistoryId()).isEqualTo(expectedHistoryId); + }); + assertThat(lifecycle.getCurrentRootKey()).isEmpty(); + } + + @Test + void shouldPreserveHistoryIdProvidedByBeforeTestStopListener() { + final String providedHistoryId = randomId(); + final TestLifecycleListener listener = new TestLifecycleListener() { + @Override + public void beforeTestStop(final TestResult result) { + result.setHistoryId(providedHistoryId); + } + }; + lifecycle = new AllureLifecycle( + writer, + new LifecycleNotifier(List.of(), List.of(listener), List.of(), List.of()) + ); + final AllureExternalKey testKey = testKey(randomId()); + lifecycle.scheduleTest( + testKey, + new TestResult() + .setName(randomName()) + .setTestCaseId(randomId()) + ); + lifecycle.startTest(testKey); + lifecycle.updateTest(result -> result.getParameters().add(new Parameter().setName("runtime").setValue("1"))); + lifecycle.stopTest(testKey); + lifecycle.writeTest(testKey); + + final ArgumentCaptor captor = forClass(TestResult.class); + verify(writer).write(captor.capture()); + assertThat(captor.getValue().getHistoryId()).isEqualTo(providedHistoryId); + } + + @Test + void shouldIgnoreExcludedAndTolerateNullHistoryParameters() { + final String testCaseId = randomId(); + final List parameters = new ArrayList<>(); + parameters.add(null); + parameters.add(new Parameter()); + parameters.add(new Parameter().setName("ignored").setValue("value").setExcluded(true)); + parameters.add(new Parameter().setName("name")); + final AllureExternalKey testKey = testKey(randomId()); + lifecycle.scheduleTest( + testKey, + new TestResult() + .setName(randomName()) + .setTestCaseId(testCaseId) + .setParameters(parameters) + ); + lifecycle.startTest(testKey); + lifecycle.stopTest(testKey); + lifecycle.writeTest(testKey); + + final ArgumentCaptor captor = forClass(TestResult.class); + verify(writer).write(captor.capture()); + assertThat(captor.getValue().getHistoryId()).isEqualTo(ResultsUtils.md5(testCaseId + "name")); + } + + @Test + void shouldCalculateHistoryIdAndCompleteStopWhenParametersAreNull() { + final String testCaseId = randomId(); + final AllureExternalKey testKey = testKey(randomId()); + lifecycle.scheduleTest( + testKey, + new TestResult() + .setName(randomName()) + .setTestCaseId(testCaseId) + .setParameters(null) + ); + lifecycle.startTest(testKey); + lifecycle.stopTest(testKey); + lifecycle.writeTest(testKey); + + final ArgumentCaptor captor = forClass(TestResult.class); + verify(writer).write(captor.capture()); + assertThat(captor.getValue()) + .hasFieldOrPropertyWithValue("stage", Stage.FINISHED) + .hasFieldOrPropertyWithValue("historyId", ResultsUtils.md5(testCaseId)); + assertThat(lifecycle.getCurrentRootKey()).isEmpty(); + } + @Test void shouldLinkScopeToTestAndFixture() { final String uuid = randomUuid(); diff --git a/allure-jbehave5/src/main/java/io/qameta/allure/jbehave5/AllureJbehave5.java b/allure-jbehave5/src/main/java/io/qameta/allure/jbehave5/AllureJbehave5.java index cf9941be1..1c173ecde 100644 --- a/allure-jbehave5/src/main/java/io/qameta/allure/jbehave5/AllureJbehave5.java +++ b/allure-jbehave5/src/main/java/io/qameta/allure/jbehave5/AllureJbehave5.java @@ -34,7 +34,6 @@ import org.jbehave.core.steps.StepCreator; import org.jbehave.core.steps.Timing; -import java.security.MessageDigest; import java.util.ArrayList; import java.util.Arrays; import java.util.Deque; @@ -46,7 +45,6 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.stream.Collectors; -import static io.qameta.allure.util.ResultsUtils.bytesToHex; import static io.qameta.allure.util.ResultsUtils.createFrameworkLabel; import static io.qameta.allure.util.ResultsUtils.createHostLabel; import static io.qameta.allure.util.ResultsUtils.createLanguageLabel; @@ -54,14 +52,12 @@ import static io.qameta.allure.util.ResultsUtils.createStoryLabel; import static io.qameta.allure.util.ResultsUtils.createThreadLabel; import static io.qameta.allure.util.ResultsUtils.createTitlePath; -import static io.qameta.allure.util.ResultsUtils.getMd5Digest; import static io.qameta.allure.util.ResultsUtils.getStatus; import static io.qameta.allure.util.ResultsUtils.getStatusDetails; -import static java.nio.charset.StandardCharsets.UTF_8; +import static io.qameta.allure.util.ResultsUtils.md5; import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; -import static java.util.Comparator.comparing; /** * Reports JBehave 5 story execution to Allure. @@ -298,16 +294,14 @@ protected void startTest(final AllureExternalKey testKey, labels.addAll(ResultsUtils.getProvidedLabels()); - final String historyId = getHistoryId(fullName, parameters); - final TestResult result = new TestResult() .setName(name) .setFullName(fullName) + .setTestCaseId(md5(fullName)) .setTitlePath(getTitlePath(story)) .setLabels(labels) .setParameters(parameters) - .setDescription(story.getDescription().asString()) - .setHistoryId(historyId); + .setDescription(story.getDescription().asString()); getLifecycle().scheduleTest(testKey, result); getLifecycle().startTest(testKey); @@ -340,26 +334,6 @@ protected boolean notParameterised(final Scenario scenario) { return scenario.getExamplesTable().getRowCount() == 0; } - /** - * Returns the history id. - * - * @param fullName the fully qualified test name - * @param parameters the Allure parameters associated with the test - * @return the history id - */ - protected String getHistoryId(final String fullName, final List parameters) { - final MessageDigest digest = getMd5Digest(); - digest.update(fullName.getBytes(UTF_8)); - parameters.stream() - .sorted(comparing(Parameter::getName).thenComparing(Parameter::getValue)) - .forEachOrdered(parameter -> { - digest.update(parameter.getName().getBytes(UTF_8)); - digest.update(parameter.getValue().getBytes(UTF_8)); - }); - final byte[] bytes = digest.digest(); - return bytesToHex(bytes); - } - private boolean isGivenStory() { return !givenStories.get().isEmpty(); } diff --git a/allure-jbehave5/src/test/java/io/qameta/allure/jbehave5/AllureJbehave5Test.java b/allure-jbehave5/src/test/java/io/qameta/allure/jbehave5/AllureJbehave5Test.java index 8dc8b3b63..90976e539 100644 --- a/allure-jbehave5/src/test/java/io/qameta/allure/jbehave5/AllureJbehave5Test.java +++ b/allure-jbehave5/src/test/java/io/qameta/allure/jbehave5/AllureJbehave5Test.java @@ -49,6 +49,7 @@ import java.util.List; import static io.qameta.allure.Allure.step; +import static io.qameta.allure.util.ResultsUtils.md5; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; @@ -293,11 +294,19 @@ void shouldSupportRuntimeApiInSteps() { assertThat(results.getTestResults()) .filteredOn("name", "Runtime API") .flatExtracting(TestResult::getParameters) - .extracting(Parameter::getName, Parameter::getValue) + .extracting(Parameter::getName, Parameter::getValue, Parameter::getExcluded) .containsExactlyInAnyOrder( - tuple("test param", "param value") + tuple("test param", "param value", null), + tuple("excluded param", "excluded value", true) ); + final TestResult testResult = results.getTestResults().get(0); + final String fullName = "runtimeapi.story: Runtime API"; + assertThat(testResult.getTestCaseId()) + .isEqualTo(md5(fullName)); + assertThat(testResult.getHistoryId()) + .isEqualTo(md5(md5(fullName) + "test param" + "param value")); + assertThat(results.getTestResults()) .filteredOn("name", "Runtime API") .flatExtracting(TestResult::getSteps) diff --git a/allure-jbehave5/src/test/java/io/qameta/allure/jbehave5/samples/RuntimeApiSteps.java b/allure-jbehave5/src/test/java/io/qameta/allure/jbehave5/samples/RuntimeApiSteps.java index 8536e84fe..305d4aabc 100644 --- a/allure-jbehave5/src/test/java/io/qameta/allure/jbehave5/samples/RuntimeApiSteps.java +++ b/allure-jbehave5/src/test/java/io/qameta/allure/jbehave5/samples/RuntimeApiSteps.java @@ -24,6 +24,7 @@ public class RuntimeApiSteps { public void given() { Allure.label("jbehave-test-label", "some-value"); Allure.parameter("test param", "param value"); + Allure.parameter("excluded param", "excluded value", true); Allure.step("sub step 1"); Allure.step("sub step 2", () -> { }); diff --git a/allure-junit-platform/src/main/java/io/qameta/allure/junitplatform/AllureJunitPlatform.java b/allure-junit-platform/src/main/java/io/qameta/allure/junitplatform/AllureJunitPlatform.java index e94a819c3..2a7c9773e 100644 --- a/allure-junit-platform/src/main/java/io/qameta/allure/junitplatform/AllureJunitPlatform.java +++ b/allure-junit-platform/src/main/java/io/qameta/allure/junitplatform/AllureJunitPlatform.java @@ -50,7 +50,6 @@ import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; -import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -79,7 +78,6 @@ import static io.qameta.allure.util.ResultsUtils.createTestClassLabel; import static io.qameta.allure.util.ResultsUtils.createTestMethodLabel; import static io.qameta.allure.util.ResultsUtils.createThreadLabel; -import static io.qameta.allure.util.ResultsUtils.getMd5Digest; import static io.qameta.allure.util.ResultsUtils.getProvidedLabels; import static java.nio.charset.StandardCharsets.UTF_8; @@ -555,12 +553,12 @@ private void startTest(final TestIdentifier testIdentifier, ? maybeParent.map(TestIdentifier::getDisplayName) .orElseGet(testIdentifier::getDisplayName) : testIdentifier.getDisplayName() - ) - .setHistoryId(getHistoryId(testIdentifier)); + ); if (parameterized) { - // history id is ignored in Allure TestOps, so we add a hidden parameter - // to make sure results of different invocations are not considered as retries + // The platform listener cannot observe Jupiter invocation arguments without the Allure Jupiter + // extension. Keep the full invocation id as a hidden fallback parameter so invocations that share + // the logical test case id can still be distinguished. result.getParameters().add( new Parameter() .setMode(Parameter.Mode.HIDDEN) @@ -781,21 +779,6 @@ private List getParents(final TestIdentifier testIdentifier) { return result; } - /** - * Returns the history id. - * - * @param testIdentifier the test identifier - * @return the history id - */ - protected String getHistoryId(final TestIdentifier testIdentifier) { - return md5(testIdentifier.getUniqueId()); - } - - private String md5(final String source) { - final byte[] bytes = getMd5Digest().digest(source.getBytes(UTF_8)); - return new BigInteger(1, bytes).toString(16); - } - private Optional getSeverity(final AnnotatedElement annotatedElement) { return getAnnotations(annotatedElement, Severity.class) .map(Severity::value) diff --git a/allure-junit-platform/src/test/java/io/qameta/allure/junitplatform/AllureJunitPlatformTest.java b/allure-junit-platform/src/test/java/io/qameta/allure/junitplatform/AllureJunitPlatformTest.java index a3bdb4be5..d65dc33bc 100644 --- a/allure-junit-platform/src/test/java/io/qameta/allure/junitplatform/AllureJunitPlatformTest.java +++ b/allure-junit-platform/src/test/java/io/qameta/allure/junitplatform/AllureJunitPlatformTest.java @@ -40,6 +40,7 @@ import io.qameta.allure.junitplatform.features.PassedTests; import io.qameta.allure.junitplatform.features.RepeatedTests; import io.qameta.allure.junitplatform.features.ReportEntryParameter; +import io.qameta.allure.junitplatform.features.RuntimeParametersTest; import io.qameta.allure.junitplatform.features.SeverityTest; import io.qameta.allure.junitplatform.features.SkippedInBeforeAllTests; import io.qameta.allure.junitplatform.features.SkippedTests; @@ -77,6 +78,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.parallel.ResourceLock; +import org.junit.platform.engine.UniqueId; import org.junit.platform.engine.discovery.DiscoverySelectors; import org.junit.platform.launcher.Launcher; import org.junit.platform.launcher.LauncherDiscoveryRequest; @@ -96,6 +98,7 @@ import static io.qameta.allure.junitplatform.features.TaggedTests.METHOD_TAG; import static io.qameta.allure.test.AllurePredicates.hasLabel; import static io.qameta.allure.test.AllurePredicates.hasStatus; +import static io.qameta.allure.test.AllureTestCommonsUtils.expectedHistoryId; import static io.qameta.allure.util.ResultsUtils.ALLURE_ID_LABEL_NAME; import static io.qameta.allure.util.ResultsUtils.EPIC_LABEL_NAME; import static io.qameta.allure.util.ResultsUtils.FEATURE_LABEL_NAME; @@ -119,6 +122,25 @@ @IsolatedLifecycle public class AllureJunitPlatformTest { + @Test + @AllureFeatures.History + @AllureFeatures.Parameters + void shouldCalculateHistoryIdFromRuntimeParametersAtTestEnd() { + final AllureResults results = runClasses(RuntimeParametersTest.class); + final String testIdentifier = "[engine:junit-jupiter]" + + "/[class:io.qameta.allure.junitplatform.features.RuntimeParametersTest]" + + "/[method:runtimeParameters()]"; + + assertThat(results.getTestResults()) + .singleElement() + .satisfies(result -> { + assertThat(result.getName()).isEqualTo("runtimeParameters()"); + assertThat(result.getTestCaseId()).isEqualTo(testIdentifier); + assertThat(result.getHistoryId()) + .isEqualTo(expectedHistoryId(result.getTestCaseId(), result.getParameters())); + }); + } + @Test @AllureFeatures.FullName void shouldSetFullName() { @@ -367,6 +389,33 @@ void shouldProcessParametrisedTests() { ); } + @Test + @AllureFeatures.History + @AllureFeatures.Parameters + void shouldUseTemplateIdAndHiddenInvocationIdForHistory() { + final AllureResults results = runClasses(ParameterisedTests.class); + final List testResults = results.getTestResults(); + + assertThat(testResults).hasSize(2); + testResults.forEach(testResult -> { + assertThat(testResult.getParameters()).hasSize(1); + final Parameter invocationId = testResult.getParameters().get(0); + assertThat(invocationId.getName()).isEqualTo("UniqueId"); + assertThat(invocationId.getMode()).isEqualTo(Parameter.Mode.HIDDEN); + assertThat(invocationId.getExcluded()).isNotEqualTo(true); + + final String templateId = UniqueId.parse(invocationId.getValue()) + .removeLastSegment() + .toString(); + assertThat(testResult.getTestCaseId()).isEqualTo(templateId); + assertThat(testResult.getHistoryId()) + .isEqualTo(expectedHistoryId(templateId, testResult.getParameters())); + }); + assertThat(testResults) + .extracting(TestResult::getHistoryId) + .doesNotHaveDuplicates(); + } + @Test @AllureFeatures.Steps void shouldAddSteps() { @@ -1234,6 +1283,7 @@ void shouldAttachPublishedFiles(@TempDir final Path outputDir) { } @Test + @AllureFeatures.History @AllureFeatures.Parameters void shouldProcessParameterizedClassInvocations() { final AllureResults results = runClasses(ParameterisedClassTests.class); @@ -1278,10 +1328,18 @@ void shouldProcessParameterizedClassInvocations() { .collect(Collectors.toList()); assertThat(uniqueIdParameters) .hasSize(4) - .allMatch(parameter -> Parameter.Mode.HIDDEN.equals(parameter.getMode())); + .allMatch(parameter -> Parameter.Mode.HIDDEN.equals(parameter.getMode())) + .allMatch(parameter -> !Boolean.TRUE.equals(parameter.getExcluded())); assertThat(uniqueIdParameters) .extracting(Parameter::getValue) .doesNotHaveDuplicates(); + testResults.forEach( + testResult -> assertThat(testResult.getHistoryId()) + .isEqualTo(expectedHistoryId(testResult.getTestCaseId(), testResult.getParameters())) + ); + assertThat(testResults) + .extracting(TestResult::getHistoryId) + .doesNotHaveDuplicates(); } @Test diff --git a/allure-junit-platform/src/test/java/io/qameta/allure/junitplatform/features/RuntimeParametersTest.java b/allure-junit-platform/src/test/java/io/qameta/allure/junitplatform/features/RuntimeParametersTest.java new file mode 100644 index 000000000..aea515a2f --- /dev/null +++ b/allure-junit-platform/src/test/java/io/qameta/allure/junitplatform/features/RuntimeParametersTest.java @@ -0,0 +1,28 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.junitplatform.features; + +import io.qameta.allure.Allure; +import org.junit.jupiter.api.Test; + +public class RuntimeParametersTest { + + @Test + void runtimeParameters() { + Allure.parameter("runtime", "included"); + Allure.parameter("ignored", "excluded", true); + } +} diff --git a/allure-junit4/src/main/java/io/qameta/allure/junit4/AllureJunit4.java b/allure-junit4/src/main/java/io/qameta/allure/junit4/AllureJunit4.java index 8b4a38c51..b2ff1fbf0 100644 --- a/allure-junit4/src/main/java/io/qameta/allure/junit4/AllureJunit4.java +++ b/allure-junit4/src/main/java/io/qameta/allure/junit4/AllureJunit4.java @@ -137,8 +137,7 @@ public void testFinished(final Description description) { } }); - getLifecycle().stopTest(testKey); - getLifecycle().writeTest(testKey); + stopAndWriteTest(testKey); } /** @@ -185,11 +184,10 @@ public void testIgnored(final Description description) { final TestResult result = createTestResult(description); result.setStatus(Status.SKIPPED); result.setStatusDetails(getIgnoredMessage(description)); - result.setStart(System.currentTimeMillis()); getLifecycle().scheduleTest(testKey, result); - getLifecycle().stopTest(testKey); - getLifecycle().writeTest(testKey); + getLifecycle().startTest(testKey); + stopAndWriteTest(testKey); } private Optional getDisplayName(final Description result) { @@ -250,8 +248,13 @@ private List