Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion allure-cucumber7-jvm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ Use this module when your BDD tests run on Cucumber JVM 7 and you want features,

- Allure Java 3.x requires Java 17 or newer.
- This module targets Cucumber JVM 7.x.
- The current build validates against Cucumber JVM 7.34.3 and Gherkin 36.1.0.
- The main test suite validates against Cucumber JVM 7.34.7, and a focused compatibility test validates the
metadata-label tag contract against the oldest runtime supported by the current adapter API, 7.3.0.

## Installation

Expand Down
38 changes: 38 additions & 0 deletions allure-cucumber7-jvm/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
description = "Allure CucumberJVM 7.0"

val cucumberVersion = "7.34.7"
val minimumCucumberVersion = "7.3.0"

dependencies {
api(project(":allure-java-commons"))
Expand Down Expand Up @@ -35,4 +36,41 @@ tasks.jar {

tasks.test {
useJUnitPlatform()
systemProperty("allure.test.cucumber.version", cucumberVersion)
}

val minimumCucumberTestRuntimeClasspath = configurations.testRuntimeClasspath.get().copyRecursive().apply {
resolutionStrategy.eachDependency {
if (requested.group == "io.cucumber" && requested.name == "cucumber-bom") {
useVersion(minimumCucumberVersion)
because("7.3.0 is the oldest Cucumber JVM runtime supported by the current adapter API")
}
}
}

val cucumber7MinimumVersionTest = tasks.register<Test>("cucumber7MinimumVersionTest") {
description = "Runs the metadata-label tag regression against Cucumber JVM $minimumCucumberVersion"
group = "verification"
dependsOn(tasks.testClasses)
mustRunAfter(tasks.test)

testClassesDirs = sourceSets.test.get().output.classesDirs
classpath = sourceSets.main.get().output + sourceSets.test.get().output + minimumCucumberTestRuntimeClasspath
useJUnitPlatform()
filter {
includeTestsMatching(
"io.qameta.allure.cucumber7jvm.AllureCucumber7JvmTest.shouldPreferMetadataTagHierarchyOverDefaults"
)
}

val standardTest = tasks.test.get()
systemProperties(standardTest.systemProperties)
systemProperty("allure.test.cucumber.version", minimumCucumberVersion)
jvmArgs = standardTest.jvmArgs
maxHeapSize = standardTest.maxHeapSize
maxParallelForks = standardTest.maxParallelForks
}

tasks.check {
dependsOn(cucumber7MinimumVersionTest)
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@
import java.util.stream.Collectors;

import static io.qameta.allure.util.ResultsUtils.PACKAGE_LABEL_NAME;
import static io.qameta.allure.util.ResultsUtils.PARENT_SUITE_LABEL_NAME;
import static io.qameta.allure.util.ResultsUtils.SUB_SUITE_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.TEST_METHOD_LABEL_NAME;
Expand All @@ -71,6 +73,8 @@
@IsolatedLifecycle
class AllureCucumber7JvmTest {

private static final String EXPECTED_CUCUMBER_VERSION_PROPERTY = "allure.test.cucumber.version";

@AllureFeatures.Base
@Test
void shouldSetName() {
Expand Down Expand Up @@ -376,6 +380,46 @@ void shouldAddTags() {
);
}

/**
* Metadata tags must replace Cucumber's generated hierarchy defaults so report generation cannot place the
* same scenario under both hierarchies.
*/
@Description
@Test
void shouldPreferMetadataTagHierarchyOverDefaults() {
assertThat(Runtime.class.getPackage().getImplementationVersion())
.as("Cucumber runtime version")
.isEqualTo(System.getProperty(EXPECTED_CUCUMBER_VERSION_PROPERTY));

final AllureResults results = runFeature("features/metadata-tags.feature");

final List<TestResult> testResults = results.getTestResults();
assertThat(testResults)
.hasSize(3)
.allSatisfy(testResult -> {
assertThat(testResult.getLabels())
.filteredOn(
label -> PARENT_SUITE_LABEL_NAME.equals(label.getName())
|| SUITE_LABEL_NAME.equals(label.getName())
|| SUB_SUITE_LABEL_NAME.equals(label.getName())
)
.extracting(Label::getName, Label::getValue)
.containsExactlyInAnyOrder(
tuple(PARENT_SUITE_LABEL_NAME, "Cucumber"),
tuple(SUITE_LABEL_NAME, "Petstore"),
tuple(SUB_SUITE_LABEL_NAME, "CRUD")
);
assertThat(testResult.getLabels())
.filteredOn(label -> "tag".equals(label.getName()))
.extracting(Label::getValue)
.doesNotContain(
"allure.label.parentSuite:Cucumber",
"allure.label.suite:Petstore",
"allure.label.subSuite:CRUD"
);
});
}

@AllureFeatures.Links
@ExtendWith(SystemPropertyExtension.class)
@SystemProperty(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@allure.label.parentSuite:Cucumber
@allure.label.suite:Petstore
@allure.label.subSuite:CRUD
Feature: Arithmetic_operations

Scenario Outline: Addition
Given a is <a>
And b is <b>
When I add a to b
Then result is <result>

Examples:
| a | b | result |
| 1 | 1 | 2 |
| 2 | 1 | 3 |
| 2 | 7 | 9 |
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,13 @@
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import static io.qameta.allure.AllureConstants.ATTACHMENT_FILE_SUFFIX;
import static io.qameta.allure.util.ResultsUtils.TAG_LABEL_NAME;
import static io.qameta.allure.util.ResultsUtils.createLabel;
import static io.qameta.allure.util.ResultsUtils.firstNonEmpty;
import static io.qameta.allure.util.ResultsUtils.getStatus;
import static io.qameta.allure.util.ResultsUtils.getStatusDetails;
Expand Down Expand Up @@ -101,6 +105,10 @@ public class AllureLifecycle {

private static final Logger LOGGER = LoggerFactory.getLogger(AllureLifecycle.class);

private static final Pattern ALLURE_LABEL_TAG = Pattern.compile(
"^@?allure\\.label\\.(?<name>.+)[:=](?<value>.+)$"
);

private static final String EXTERNAL_KEY = "external key";

private static final String KEY_NOT_FOUND = "Could not {}: item with key {} not found";
Expand Down Expand Up @@ -346,8 +354,8 @@ public void updateTest(final Consumer<TestResult> update) {
* Registers default labels for the test with given key. Default labels do not appear on the test result until
* the test stops: {@link #stopTest(AllureExternalKey)} merges them after scope metadata, adding, for each
* distinct label name, the default labels with that name only when the test has no labels with that name by
* then. Labels provided by the user — through annotations, the runtime API, or before fixtures — thus take
* precedence over defaults instead of being duplicated by them. Repeated calls accumulate.
* then. Labels provided by the user — through annotations, framework tags, the runtime API, or before fixtures
* — thus take precedence over defaults instead of being duplicated by them. Repeated calls accumulate.
*
* <p>Intended for the framework-computed grouping labels a user may legitimately override: the suite family
* and BDD structure labels. System labels — framework, language, host, thread, package, testClass, testMethod
Expand All @@ -366,11 +374,12 @@ public void addDefaultLabels(final AllureExternalKey key, final Collection<Label
}

/**
* Stops test by given key. The test must be running; scope metadata is merged into the test here, then default
* labels are applied for each label name the test still has no labels for. 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.
* Stops test by given key. The test must be running; scope metadata is merged into the test here, Allure label
* metadata carried by framework tags is promoted to labels, then default labels are applied for each label name
* the test still has no labels for. 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
*/
Expand All @@ -395,6 +404,7 @@ public void stopTest(final AllureExternalKey key) {
testResult.setParameters(new ArrayList<>());
}
applyScopeMetadata(item);
applyAllureLabelTags(testResult);
applyDefaultLabels(item);
if (Objects.isNull(testResult.getHistoryId()) && Objects.nonNull(testResult.getTestCaseId())) {
testResult.setHistoryId(calculateHistoryId(testResult.getTestCaseId(), testResult.getParameters()));
Expand Down Expand Up @@ -1347,10 +1357,43 @@ private void applyScopeMetadata(final TestItem item) {
}
}

/**
* Replaces tag labels that use the report's {@code allure.label.<name>[:=]<value>} convention with their
* represented labels. Processing the convention before defaults are selected lets metadata from every
* lifecycle-based framework adapter override same-name grouping defaults, while consuming the source tag keeps
* report generation from applying it a second time.
*/
private static void applyAllureLabelTags(final TestResult testResult) {
final List<Label> labels = testResult.getLabels();
if (Objects.isNull(labels) || labels.isEmpty()) {
return;
}
final List<Label> normalizedLabels = new ArrayList<>(labels.size());
for (final Label label : labels) {
if (Objects.isNull(label) || !TAG_LABEL_NAME.equals(label.getName())
|| Objects.isNull(label.getValue())) {
normalizedLabels.add(label);
continue;
}
final Matcher matcher = ALLURE_LABEL_TAG.matcher(label.getValue());
if (matcher.matches()) {
normalizedLabels.add(
createLabel(
matcher.group("name"),
matcher.group("value").replace("_", " ")
)
);
} else {
normalizedLabels.add(label);
}
}
testResult.setLabels(normalizedLabels);
}

/**
* Adds the test's registered default labels — for each distinct label name, only when the test has no labels
* with that name. Runs after scope metadata is merged, so labels set from before fixtures also take precedence
* over defaults.
* with that name. Runs after scope metadata is merged and Allure label tags are promoted, so framework tags and
* labels set from before fixtures also take precedence over defaults.
*/
private static void applyDefaultLabels(final TestItem item) {
if (item.defaultLabels().isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,61 @@ void shouldApplyDefaultLabelsWhenTestStops() {
});
}

/**
* Allure label metadata carried by framework tag labels must be promoted before fallback labels are chosen,
* so every lifecycle-based adapter gets custom labels without report-time duplicates.
*/
@Description
@Test
void shouldConvertAllureLabelTagsBeforeApplyingDefaults() {
final String testUuid = randomId();
final AllureExternalKey testKey = testKey(testUuid);
lifecycle.scheduleTest(
testKey,
new TestResult()
.setUuid(testUuid)
.setName(randomName())
.setLabels(
List.of(
new Label().setName("tag").setValue("smoke"),
new Label().setName("tag")
.setValue("@allure.label.parentSuite:Platform"),
new Label().setName("tag").setValue("allure.label.suite=API_Tests"),
new Label().setName("tag").setValue("allure.label.story:first_story"),
new Label().setName("tag").setValue("allure.label.story:second_story")
)
)
);
lifecycle.addDefaultLabels(
testKey,
List.of(
new Label().setName("suite").setValue("default suite"),
new Label().setName("story").setValue("default story"),
new Label().setName("feature").setValue("default feature")
)
);
lifecycle.startTest(testKey);
lifecycle.stopTest(testKey);
lifecycle.writeTest(testKey);

final ArgumentCaptor<TestResult> captor = forClass(TestResult.class);
verify(writer, times(1)).write(captor.capture());

Allure.step("Verify metadata tags replace same-name defaults", step -> {
step.parameter("test uuid", testUuid);
assertThat(captor.getValue().getLabels())
.extracting(Label::getName, Label::getValue)
.containsExactly(
tuple("tag", "smoke"),
tuple("parentSuite", "Platform"),
tuple("suite", "API Tests"),
tuple("story", "first story"),
tuple("story", "second story"),
tuple("feature", "default feature")
);
});
}

@Test
void shouldPreferUserLabelsOverDefaultLabels() {
final String testUuid = randomId();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@

import static io.qameta.allure.junitplatform.AllureJunitPlatform.JUNIT_PLATFORM_UNIQUE_ID;
import static io.qameta.allure.junitplatform.AllureJunitPlatformTestUtils.runClasses;
import static io.qameta.allure.junitplatform.features.TaggedTests.ALLURE_LABEL_TAG;
import static io.qameta.allure.junitplatform.features.TaggedTests.CLASS_TAG;
import static io.qameta.allure.junitplatform.features.TaggedTests.METHOD_TAG;
import static io.qameta.allure.test.AllurePredicates.hasLabel;
Expand Down Expand Up @@ -497,6 +498,29 @@ void shouldAddTags() {
);
}

/**
* Allure label metadata from native JUnit tags must replace generated hierarchy defaults through the shared
* lifecycle, without leaving the metadata source tag for report generation to process again.
*/
@Description
@Test
@AllureFeatures.MarkerAnnotations
void shouldConvertAllureLabelTags() {
final AllureResults results = runClasses(TaggedTests.class);

final List<TestResult> testResults = results.getTestResults();
assertThat(testResults).hasSize(1);

final List<Label> labels = testResults.get(0).getLabels();
assertThat(labels)
.filteredOn(label -> SUITE_LABEL_NAME.equals(label.getName()))
.extracting(Label::getValue)
.containsExactly("Tagged tests");
assertThat(labels)
.extracting(Label::getName, Label::getValue)
.doesNotContain(tuple(TAG_LABEL_NAME, ALLURE_LABEL_TAG));
}

@Test
@AllureFeatures.DisplayName
void shouldProcessDefaultTestClassDisplayName() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@
@Tag(TaggedTests.CLASS_TAG)
public class TaggedTests {

public static final String ALLURE_LABEL_TAG = "allure.label.suite:Tagged_tests";
public static final String CLASS_TAG = "class_tag";
public static final String METHOD_TAG = "single_tag";

@Test
@Tag(ALLURE_LABEL_TAG)
@Tag(METHOD_TAG)
void taggedTest() {
}
Expand Down
Loading
Loading