Skip to content

Commit eecb1b5

Browse files
author
Artem Eroshenko
authored
first draft of cucumber 6 support (via #475)
1 parent d2e7f6f commit eecb1b5

33 files changed

+2293
-0
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
description = "Allure CucumberJVM 6.0"
2+
3+
val agent: Configuration by configurations.creating
4+
5+
val cucumberVersion = "6.1.1"
6+
val cucumberGherkinVersion = "5.1.0"
7+
8+
dependencies {
9+
agent("org.aspectj:aspectjweaver")
10+
api(project(":allure-java-commons"))
11+
compileOnly("io.cucumber:cucumber-plugin:$cucumberVersion")
12+
implementation("io.cucumber:gherkin:$cucumberGherkinVersion")
13+
testImplementation("io.cucumber:gherkin:$cucumberGherkinVersion")
14+
testImplementation("io.cucumber:cucumber-core:$cucumberVersion")
15+
testImplementation("io.cucumber:cucumber-java:$cucumberVersion")
16+
testImplementation("io.cucumber:cucumber-testng:$cucumberVersion")
17+
testImplementation("commons-io:commons-io")
18+
testImplementation("io.github.glytching:junit-extensions")
19+
testImplementation("org.assertj:assertj-core")
20+
testImplementation("org.junit.jupiter:junit-jupiter-api")
21+
testImplementation("org.slf4j:slf4j-simple")
22+
testImplementation(project(":allure-java-commons-test"))
23+
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
24+
}
25+
26+
tasks.jar {
27+
manifest {
28+
attributes(mapOf(
29+
"Automatic-Module-Name" to "io.qameta.allure.cucumber5jvm"
30+
))
31+
}
32+
}
33+
34+
tasks.test {
35+
useJUnitPlatform()
36+
doFirst {
37+
jvmArgs("-javaagent:${agent.singleFile}")
38+
}
39+
}

allure-cucumber6-jvm/src/main/java/io/qameta/allure/cucumber6jvm/AllureCucumber6Jvm.java

Lines changed: 424 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/*
2+
* Copyright 2019 Qameta Software OÜ
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.qameta.allure.cucumber6jvm;
17+
18+
import io.cucumber.plugin.event.TestCase;
19+
import gherkin.ast.Feature;
20+
import io.qameta.allure.model.Label;
21+
import io.qameta.allure.model.Link;
22+
import io.qameta.allure.util.ResultsUtils;
23+
import org.slf4j.Logger;
24+
import org.slf4j.LoggerFactory;
25+
26+
import java.io.File;
27+
import java.net.URI;
28+
import java.util.ArrayList;
29+
import java.util.Arrays;
30+
import java.util.Deque;
31+
import java.util.List;
32+
import java.util.Objects;
33+
import java.util.Optional;
34+
import java.util.regex.Pattern;
35+
import java.util.stream.Collectors;
36+
import java.util.stream.Stream;
37+
38+
import static io.qameta.allure.util.ResultsUtils.createFeatureLabel;
39+
import static io.qameta.allure.util.ResultsUtils.createFrameworkLabel;
40+
import static io.qameta.allure.util.ResultsUtils.createHostLabel;
41+
import static io.qameta.allure.util.ResultsUtils.createLabel;
42+
import static io.qameta.allure.util.ResultsUtils.createLanguageLabel;
43+
import static io.qameta.allure.util.ResultsUtils.createStoryLabel;
44+
import static io.qameta.allure.util.ResultsUtils.createSuiteLabel;
45+
import static io.qameta.allure.util.ResultsUtils.createTestClassLabel;
46+
import static io.qameta.allure.util.ResultsUtils.createThreadLabel;
47+
48+
/**
49+
* Scenario labels and links builder.
50+
*/
51+
@SuppressWarnings({"CyclomaticComplexity", "PMD.CyclomaticComplexity", "PMD.NcssCount", "MultipleStringLiterals"})
52+
class LabelBuilder {
53+
private static final Logger LOGGER = LoggerFactory.getLogger(LabelBuilder.class);
54+
private static final String COMPOSITE_TAG_DELIMITER = "=";
55+
56+
private static final String SEVERITY = "@SEVERITY";
57+
private static final String ISSUE_LINK = "@ISSUE";
58+
private static final String TMS_LINK = "@TMSLINK";
59+
private static final String PLAIN_LINK = "@LINK";
60+
61+
private final List<Label> scenarioLabels = new ArrayList<>();
62+
private final List<Link> scenarioLinks = new ArrayList<>();
63+
64+
LabelBuilder(final Feature feature, final TestCase scenario, final Deque<String> tags) {
65+
final TagParser tagParser = new TagParser(feature, scenario);
66+
67+
while (tags.peek() != null) {
68+
final String tag = tags.remove();
69+
70+
if (tag.contains(COMPOSITE_TAG_DELIMITER)) {
71+
72+
final String[] tagParts = tag.split(COMPOSITE_TAG_DELIMITER, 2);
73+
if (tagParts.length < 2 || Objects.isNull(tagParts[1]) || tagParts[1].isEmpty()) {
74+
// skip empty tags, e.g. '@tmsLink=', to avoid formatter errors
75+
continue;
76+
}
77+
78+
final String tagKey = tagParts[0].toUpperCase();
79+
final String tagValue = tagParts[1];
80+
81+
// Handle composite named links
82+
if (tagKey.startsWith(PLAIN_LINK + ".")) {
83+
tryHandleNamedLink(tag);
84+
continue;
85+
}
86+
87+
switch (tagKey) {
88+
case SEVERITY:
89+
getScenarioLabels().add(ResultsUtils.createSeverityLabel(tagValue.toLowerCase()));
90+
break;
91+
case TMS_LINK:
92+
getScenarioLinks().add(ResultsUtils.createTmsLink(tagValue));
93+
break;
94+
case ISSUE_LINK:
95+
getScenarioLinks().add(ResultsUtils.createIssueLink(tagValue));
96+
break;
97+
case PLAIN_LINK:
98+
getScenarioLinks().add(ResultsUtils.createLink(null, tagValue, tagValue, null));
99+
break;
100+
default:
101+
LOGGER.warn("Composite tag {} is not supported. adding it as RAW", tagKey);
102+
getScenarioLabels().add(getTagLabel(tag));
103+
break;
104+
}
105+
} else if (tagParser.isPureSeverityTag(tag)) {
106+
getScenarioLabels().add(ResultsUtils.createSeverityLabel(tag.substring(1)));
107+
} else if (!tagParser.isResultTag(tag)) {
108+
getScenarioLabels().add(getTagLabel(tag));
109+
}
110+
}
111+
112+
final String featureName = feature.getName();
113+
final URI uri = scenario.getUri();
114+
115+
getScenarioLabels().addAll(Arrays.asList(
116+
createHostLabel(),
117+
createThreadLabel(),
118+
createFeatureLabel(featureName),
119+
createStoryLabel(scenario.getName()),
120+
createSuiteLabel(featureName),
121+
createTestClassLabel(scenario.getName()),
122+
createFrameworkLabel("cucumber4jvm"),
123+
createLanguageLabel("java"),
124+
createLabel("gherkin_uri", uri.toString())
125+
));
126+
127+
featurePackage(uri.toString(), featureName)
128+
.map(ResultsUtils::createPackageLabel)
129+
.ifPresent(getScenarioLabels()::add);
130+
}
131+
132+
public List<Label> getScenarioLabels() {
133+
return scenarioLabels;
134+
}
135+
136+
public List<Link> getScenarioLinks() {
137+
return scenarioLinks;
138+
}
139+
140+
private Label getTagLabel(final String tag) {
141+
return ResultsUtils.createTagLabel(tag.substring(1));
142+
}
143+
144+
/**
145+
* Handle composite named links.
146+
*
147+
* @param tagString Full tag name and value
148+
*/
149+
private void tryHandleNamedLink(final String tagString) {
150+
final String namedLinkPatternString = PLAIN_LINK + "\\.(\\w+-?)+=(\\w+(-|_)?)+";
151+
final Pattern namedLinkPattern = Pattern.compile(namedLinkPatternString, Pattern.CASE_INSENSITIVE);
152+
153+
if (namedLinkPattern.matcher(tagString).matches()) {
154+
final String type = tagString.split(COMPOSITE_TAG_DELIMITER)[0].split("[.]")[1];
155+
final String name = tagString.split(COMPOSITE_TAG_DELIMITER)[1];
156+
getScenarioLinks().add(ResultsUtils.createLink(null, name, null, type));
157+
} else {
158+
LOGGER.warn("Composite named tag {} does not match regex {}. Skipping", tagString,
159+
namedLinkPatternString);
160+
}
161+
}
162+
163+
private Optional<String> featurePackage(final String uriString, final String featureName) {
164+
final Optional<URI> maybeUri = safeUri(uriString);
165+
if (!maybeUri.isPresent()) {
166+
return Optional.empty();
167+
}
168+
URI uri = maybeUri.get();
169+
170+
if (!uri.isOpaque()) {
171+
final URI work = new File("").toURI();
172+
uri = work.relativize(uri);
173+
}
174+
final String schemeSpecificPart = uri.normalize().getSchemeSpecificPart();
175+
final Stream<String> folders = Stream.of(schemeSpecificPart.replaceAll("\\.", "_").split("/"));
176+
final Stream<String> name = Stream.of(featureName);
177+
return Optional.of(Stream.concat(folders, name)
178+
.filter(Objects::nonNull)
179+
.filter(s -> !s.isEmpty())
180+
.collect(Collectors.joining(".")));
181+
}
182+
183+
private static Optional<URI> safeUri(final String uri) {
184+
try {
185+
return Optional.of(URI.create(uri));
186+
} catch (Exception e) {
187+
LOGGER.debug("could not parse feature uri {}", uri, e);
188+
}
189+
return Optional.empty();
190+
}
191+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2019 Qameta Software OÜ
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.qameta.allure.cucumber6jvm;
17+
18+
import gherkin.ast.Feature;
19+
import io.cucumber.plugin.event.TestCase;
20+
import io.qameta.allure.SeverityLevel;
21+
22+
import java.util.Arrays;
23+
24+
/**
25+
* Parser for tags.
26+
*/
27+
class TagParser {
28+
private static final String FLAKY = "@FLAKY";
29+
private static final String KNOWN = "@KNOWN";
30+
private static final String MUTED = "@MUTED";
31+
32+
private final Feature feature;
33+
private final TestCase scenario;
34+
35+
TagParser(final Feature feature, final TestCase scenario) {
36+
this.feature = feature;
37+
this.scenario = scenario;
38+
}
39+
40+
public boolean isFlaky() {
41+
return getStatusDetailByTag(FLAKY);
42+
}
43+
44+
public boolean isMuted() {
45+
return getStatusDetailByTag(MUTED);
46+
}
47+
48+
public boolean isKnown() {
49+
return getStatusDetailByTag(KNOWN);
50+
}
51+
52+
private boolean getStatusDetailByTag(final String tagName) {
53+
return scenario.getTags().stream()
54+
.anyMatch(tag -> tag.equalsIgnoreCase(tagName))
55+
|| feature.getTags().stream()
56+
.anyMatch(tag -> tag.getName().equalsIgnoreCase(tagName));
57+
}
58+
59+
public boolean isResultTag(final String tag) {
60+
return Arrays.asList(new String[]{FLAKY, KNOWN, MUTED})
61+
.contains(tag.toUpperCase());
62+
}
63+
64+
public boolean isPureSeverityTag(final String tag) {
65+
return Arrays.stream(SeverityLevel.values())
66+
.map(SeverityLevel::value)
67+
.map(value -> "@" + value)
68+
.anyMatch(value -> value.equalsIgnoreCase(tag));
69+
}
70+
71+
}

0 commit comments

Comments
 (0)