Skip to content

Commit c56aa7a

Browse files
author
Roberto Nares
committed
CTP-11036 Send full Cucumber feature name to CTP
1 parent 1e2acac commit c56aa7a

5 files changed

Lines changed: 231 additions & 18 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,14 +281,14 @@ Each Cucumber scenario is reported as an individual test case.
281281
For example:
282282

283283
```text
284-
Feature: petclinic.feature
284+
Feature: PetClinic browser actions
285285
Scenario: Add a new pet
286286
```
287287

288288
is reported to CTP as:
289289

290290
```text
291-
test=petclinic.feature#Add a new pet
291+
test=Feature: PetClinic browser actions#Add a new pet
292292
testCase=Add a new pet
293293
```
294294
## Browser Integrations

coverage-integration-cucumber/pom.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,18 @@
2525
<scope>provided</scope>
2626
</dependency>
2727

28+
<dependency>
29+
<groupId>io.cucumber</groupId>
30+
<artifactId>gherkin</artifactId>
31+
<scope>provided</scope>
32+
</dependency>
33+
34+
<dependency>
35+
<groupId>io.cucumber</groupId>
36+
<artifactId>messages</artifactId>
37+
<scope>provided</scope>
38+
</dependency>
39+
2840
<dependency>
2941
<groupId>junit</groupId>
3042
<artifactId>junit</artifactId>

coverage-integration-cucumber/src/main/java/com/parasoft/coverage/integration/cucumber/ParasoftCucumberScenarioListener.java

Lines changed: 128 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,21 @@
1616

1717
package com.parasoft.coverage.integration.cucumber;
1818

19+
import java.io.IOException;
20+
import java.io.InputStream;
1921
import java.net.URI;
22+
import java.nio.file.Files;
23+
import java.nio.file.Path;
24+
import java.util.Map;
2025
import java.util.Objects;
26+
import java.util.concurrent.ConcurrentHashMap;
2127

28+
import io.cucumber.gherkin.GherkinParser;
2229
import io.cucumber.java.After;
2330
import io.cucumber.java.Before;
2431
import io.cucumber.java.Scenario;
2532
import io.cucumber.java.Status;
33+
import io.cucumber.messages.types.Feature;
2634

2735
import org.slf4j.Logger;
2836
import org.slf4j.LoggerFactory;
@@ -43,6 +51,8 @@ public class ParasoftCucumberScenarioListener
4351

4452
private static final int COVERAGE_HOOK_ORDER = Integer.MIN_VALUE;
4553
private static final int MAX_RESULT_MESSAGE_LENGTH = 500;
54+
private static final String CLASSPATH_SCHEME = "classpath";
55+
private static final Map<URI, String> FEATURE_NAME_CACHE = new ConcurrentHashMap<>();
4656

4757
private final CoverageApiClient coverageApiClient;
4858

@@ -178,11 +188,125 @@ void stopScenario(ResultEnum result, String resultMessage)
178188

179189
static String buildTestId(URI scenarioUri, String scenarioName)
180190
{
181-
String featureFileName = extractFeatureFileName(scenarioUri);
191+
String featureName = resolveFeatureName(scenarioUri);
182192

183-
return featureFileName == null
184-
? scenarioName
185-
: featureFileName + '#' + scenarioName;
193+
if (featureName == null) {
194+
featureName = extractFeatureFileName(scenarioUri);
195+
}
196+
197+
return featureName == null ? scenarioName : featureName + '#' + scenarioName;
198+
}
199+
200+
static String resolveFeatureName(URI uri)
201+
{
202+
if (uri == null) {
203+
return null;
204+
}
205+
206+
String cachedFeatureName = FEATURE_NAME_CACHE.get(uri);
207+
208+
if (cachedFeatureName != null) {
209+
return cachedFeatureName;
210+
}
211+
212+
String resolvedFeatureName = parseFeatureName(uri);
213+
214+
if (resolvedFeatureName == null) {
215+
return null;
216+
}
217+
218+
String existingFeatureName = FEATURE_NAME_CACHE.putIfAbsent(uri, resolvedFeatureName);
219+
220+
return existingFeatureName == null ? resolvedFeatureName : existingFeatureName;
221+
}
222+
223+
private static String parseFeatureName(URI uri)
224+
{
225+
try (InputStream featureSource = openFeatureSource(uri)) {
226+
GherkinParser parser = GherkinParser.builder()
227+
.includeSource(false)
228+
.includeGherkinDocument(true)
229+
.includePickles(false)
230+
.build();
231+
232+
return parser.parse(uri.toString(), featureSource)
233+
.flatMap(envelope -> envelope.getGherkinDocument().stream())
234+
.flatMap(gherkinDocument -> gherkinDocument.getFeature().stream())
235+
.map(ParasoftCucumberScenarioListener::formatFeatureName)
236+
.filter(Objects::nonNull)
237+
.findFirst()
238+
.orElse(null);
239+
}
240+
catch (IOException | RuntimeException e) {
241+
LOGGER.warn("Unable to resolve Cucumber feature name from {}; using the feature file name instead", uri, e);
242+
243+
return null;
244+
}
245+
}
246+
247+
private static String formatFeatureName(Feature feature)
248+
{
249+
String featureName = feature.getName();
250+
251+
if (featureName == null || featureName.isBlank()) {
252+
return null;
253+
}
254+
255+
String normalizedFeatureName = featureName.strip();
256+
String featureKeyword = feature.getKeyword();
257+
258+
if (featureKeyword == null || featureKeyword.isBlank()) {
259+
return normalizedFeatureName;
260+
}
261+
262+
String normalizedFeatureKeyword = featureKeyword.strip();
263+
264+
return normalizedFeatureKeyword.endsWith(":")
265+
? normalizedFeatureKeyword + ' ' + normalizedFeatureName
266+
: normalizedFeatureKeyword + ": " + normalizedFeatureName;
267+
}
268+
269+
private static InputStream openFeatureSource(URI uri) throws IOException
270+
{
271+
String scheme = uri.getScheme();
272+
273+
if (CLASSPATH_SCHEME.equalsIgnoreCase(scheme)) {
274+
String resourceName = uri.getSchemeSpecificPart();
275+
276+
while (resourceName != null && resourceName.startsWith("/")) {
277+
resourceName = resourceName.substring(1);
278+
}
279+
280+
if (resourceName == null || resourceName.isBlank()) {
281+
throw new IOException("Cucumber classpath feature URI has no resource name: " + uri);
282+
}
283+
284+
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
285+
286+
if (classLoader == null) {
287+
classLoader = ParasoftCucumberScenarioListener.class.getClassLoader();
288+
}
289+
290+
InputStream featureSource = classLoader.getResourceAsStream(resourceName);
291+
292+
if (featureSource == null) {
293+
throw new IOException("Unable to open Cucumber feature resource: " + uri);
294+
}
295+
296+
return featureSource;
297+
}
298+
299+
if (scheme == null || scheme.isBlank()) {
300+
String path = uri.getPath();
301+
302+
if (path == null || path.isBlank()) {
303+
path = uri.toString();
304+
}
305+
306+
return Files.newInputStream(Path.of(path));
307+
}
308+
309+
return uri.toURL().openStream();
186310
}
187311

188312
static String extractFeatureFileName(URI uri)

coverage-integration-cucumber/src/test/java/com/parasoft/coverage/integration/cucumber/ParasoftCucumberScenarioListenerTest.java

Lines changed: 77 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
import static org.junit.Assert.assertNull;
2121

2222
import java.net.URI;
23+
import java.net.URL;
24+
import java.net.URLClassLoader;
25+
import java.nio.file.Files;
26+
import java.nio.file.Path;
2327

2428
import io.cucumber.java.Status;
2529

@@ -33,22 +37,83 @@
3337
public class ParasoftCucumberScenarioListenerTest
3438
{
3539
@Test
36-
public void buildsTestIdentifierFromFeatureFileAndScenarioName()
40+
public void buildsTestIdentifierFromClasspathFeatureNameAndScenarioName() throws Exception
41+
{
42+
Path classpathRoot = Files.createTempDirectory("cucumber-feature-classpath-");
43+
Path featureFile = classpathRoot.resolve("features/coverage-reporting.feature");
44+
45+
Files.createDirectories(featureFile.getParent());
46+
Files.writeString(
47+
featureFile,
48+
"""
49+
Feature: Cucumber coverage reporting
50+
51+
Scenario: Reports feature name
52+
Given the test is running
53+
""");
54+
55+
ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
56+
57+
try (URLClassLoader classLoader = new URLClassLoader(new URL[] { classpathRoot.toUri().toURL() }, originalClassLoader)) {
58+
Thread.currentThread().setContextClassLoader(classLoader);
59+
60+
assertEquals(
61+
"Feature: Cucumber coverage reporting#Reports feature name",
62+
ParasoftCucumberScenarioListener.buildTestId(
63+
URI.create(
64+
"classpath:features/coverage-reporting.feature"),
65+
"Reports feature name"));
66+
}
67+
finally {
68+
Thread.currentThread().setContextClassLoader(originalClassLoader);
69+
70+
Files.deleteIfExists(featureFile);
71+
Files.deleteIfExists(featureFile.getParent());
72+
Files.deleteIfExists(classpathRoot);
73+
}
74+
}
75+
76+
@Test
77+
public void resolvesFeatureNameFromFileFeature() throws Exception
78+
{
79+
Path featureFile = Files.createTempFile("coverage-reporting-", ".feature");
80+
81+
Files.writeString(
82+
featureFile,
83+
"""
84+
Feature: File-based coverage reporting
85+
86+
Scenario: Reports feature name
87+
Given the test is running
88+
""");
89+
90+
try {
91+
assertEquals(
92+
"Feature: File-based coverage reporting",
93+
ParasoftCucumberScenarioListener.resolveFeatureName(featureFile.toUri()));
94+
}
95+
finally {
96+
Files.deleteIfExists(featureFile);
97+
}
98+
}
99+
100+
@Test
101+
public void fallsBackToFeatureFileNameWhenFeatureCannotBeRead()
37102
{
38103
assertEquals(
39-
"petclinic.feature#Navigate to home page",
104+
"missing.feature#Reports feature name",
40105
ParasoftCucumberScenarioListener.buildTestId(
41-
URI.create("classpath:features/petclinic.feature"),
42-
"Navigate to home page"));
106+
URI.create("classpath:features/missing.feature"),
107+
"Reports feature name"));
43108
}
44109

45110
@Test
46111
public void extractsFeatureFileFromFileUri()
47112
{
48113
assertEquals(
49-
"parabank-demo.feature",
114+
"coverage-reporting.feature",
50115
ParasoftCucumberScenarioListener.extractFeatureFileName(
51-
URI.create("file:///tmp/features/parabank-demo.feature")));
116+
URI.create("file:///tmp/features/coverage-reporting.feature")));
52117
}
53118

54119
@Test
@@ -93,8 +158,8 @@ public void startsAndStopsScenarioAndManagesExecutionContext()
93158

94159
try {
95160
listener.startScenario(
96-
"petclinic.feature#Navigate to home page",
97-
"Navigate to home page");
161+
"Feature: Cucumber coverage reporting#Reports feature name",
162+
"Reports feature name");
98163

99164
assertEquals(
100165
"test-operator-id=automation-user+parallel-123",
@@ -103,16 +168,16 @@ public void startsAndStopsScenarioAndManagesExecutionContext()
103168
listener.stopScenario(ResultEnum.PASS, null);
104169

105170
assertEquals(
106-
"petclinic.feature#Navigate to home page",
171+
"Feature: Cucumber coverage reporting#Reports feature name",
107172
client.startedTest);
108173
assertEquals(
109-
"Navigate to home page",
174+
"Reports feature name",
110175
client.startedTestCase);
111176
assertEquals(
112-
"petclinic.feature#Navigate to home page",
177+
"Feature: Cucumber coverage reporting#Reports feature name",
113178
client.stoppedTest);
114179
assertEquals(
115-
"Navigate to home page",
180+
"Reports feature name",
116181
client.stoppedTestCase);
117182
assertEquals(ResultEnum.PASS, client.result);
118183
assertNull(client.message);

pom.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
<gsonfire.version>1.9.0</gsonfire.version>
3333
<jakarta.annotation.version>3.0.0</jakarta.annotation.version>
3434
<cucumber.version>7.34.4</cucumber.version>
35+
<gherkin.version>36.1.0</gherkin.version>
36+
<messages.version>30.1.0</messages.version>
3537
<junit4.version>4.13.2</junit4.version>
3638
<junit5.version>5.14.2</junit5.version>
3739
<maven.javadoc.version>3.11.2</maven.javadoc.version>
@@ -75,6 +77,16 @@
7577
<artifactId>cucumber-java</artifactId>
7678
<version>${cucumber.version}</version>
7779
</dependency>
80+
<dependency>
81+
<groupId>io.cucumber</groupId>
82+
<artifactId>gherkin</artifactId>
83+
<version>${gherkin.version}</version>
84+
</dependency>
85+
<dependency>
86+
<groupId>io.cucumber</groupId>
87+
<artifactId>messages</artifactId>
88+
<version>${messages.version}</version>
89+
</dependency>
7890
<dependency>
7991
<groupId>com.microsoft.playwright</groupId>
8092
<artifactId>playwright</artifactId>

0 commit comments

Comments
 (0)