From 5f898099f66b02e5dcb7345cdedb13b49f6a9e51 Mon Sep 17 00:00:00 2001 From: timgrohmann Date: Wed, 10 Sep 2025 13:30:25 +0200 Subject: [PATCH] feat: Add Requirement Tag for XRay Import --- .../junitxmlformatter/XmlReportData.java | 15 +++++++++++++++ .../junitxmlformatter/XmlReportWriter.java | 18 ++++++++++++++++-- javascript/src/index.ts | 7 +++++++ javascript/src/makeReport.ts | 6 ++++++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/java/src/main/java/io/cucumber/junitxmlformatter/XmlReportData.java b/java/src/main/java/io/cucumber/junitxmlformatter/XmlReportData.java index fffaeb5..05a3b43 100644 --- a/java/src/main/java/io/cucumber/junitxmlformatter/XmlReportData.java +++ b/java/src/main/java/io/cucumber/junitxmlformatter/XmlReportData.java @@ -18,6 +18,7 @@ import java.time.Duration; import java.util.AbstractMap.SimpleEntry; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; @@ -35,6 +36,7 @@ class XmlReportData { private final NamingStrategy namingStrategy; private static final long MILLIS_PER_SECOND = SECONDS.toMillis(1L); + private static final String REQUIREMENT_PREFIX = "@Req:"; XmlReportData(NamingStrategy namingStrategy) { this.namingStrategy = namingStrategy; @@ -83,6 +85,19 @@ String getFeatureName(TestCaseStarted testCaseStarted) { .orElseGet(() -> this.getPickle(testCaseStarted).getUri()); } + /** + * Extracts a requirement tag denoted by starting with "@Req:" + */ + List getFeatureRequirements(TestCaseStarted testCaseStarted) { + return query.findPickleBy(testCaseStarted) + .map(Pickle::getTags) + .orElse(Collections.emptyList()) + .stream() + .filter(tag -> tag.getName().startsWith(REQUIREMENT_PREFIX)) + .map(tag -> tag.getName().substring(REQUIREMENT_PREFIX.length())) + .collect(toList()); + } + List> getStepsAndResult(TestCaseStarted testCaseStarted) { return query.findTestStepFinishedAndTestStepBy(testCaseStarted) .stream() diff --git a/java/src/main/java/io/cucumber/junitxmlformatter/XmlReportWriter.java b/java/src/main/java/io/cucumber/junitxmlformatter/XmlReportWriter.java index 040b27d..edc6f03 100644 --- a/java/src/main/java/io/cucumber/junitxmlformatter/XmlReportWriter.java +++ b/java/src/main/java/io/cucumber/junitxmlformatter/XmlReportWriter.java @@ -5,11 +5,11 @@ import io.cucumber.messages.types.TestStepResult; import io.cucumber.messages.types.TestStepResultStatus; -import java.util.List; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; import java.io.Writer; import java.util.EnumSet; +import java.util.List; import java.util.Map; import java.util.Optional; @@ -78,6 +78,7 @@ private void writeTestcase(EscapingXmlStreamWriter writer, TestCaseStarted testC writer.writeStartElement("testcase"); writeTestCaseAttributes(writer, testCaseStarted); writer.writeNewLine(); + writeRequirementProperties(writer, testCaseStarted); writeNonPassedElement(writer, testCaseStarted); writeStepAndResultList(writer, testCaseStarted); writer.writeEndElement(); @@ -90,6 +91,19 @@ private void writeTestCaseAttributes(EscapingXmlStreamWriter writer, TestCaseSta writer.writeAttribute("time", String.valueOf(data.getDurationInSeconds(testCaseStarted))); } + private void writeRequirementProperties(EscapingXmlStreamWriter writer, TestCaseStarted testCaseStarted) throws XMLStreamException { + List requirements = data.getFeatureRequirements(testCaseStarted); + + if (!requirements.isEmpty()) { + writer.writeStartElement("properties"); + writer.writeStartElement("property"); + writer.writeAttribute("name", "requirements"); + writer.writeAttribute("value", String.join(",", requirements)); + writer.writeNewLine(); + } + + } + private void writeNonPassedElement(EscapingXmlStreamWriter writer, TestCaseStarted testCaseStarted) throws XMLStreamException { TestStepResult result = data.getTestCaseStatus(testCaseStarted); TestStepResultStatus status = result.getStatus(); @@ -164,4 +178,4 @@ private static String createStepResultList(List> resul }); return sb.toString(); } -} \ No newline at end of file +} diff --git a/javascript/src/index.ts b/javascript/src/index.ts index b94874d..bd7e685 100644 --- a/javascript/src/index.ts +++ b/javascript/src/index.ts @@ -41,6 +41,13 @@ export default { name: testCase.name, time: testCase.time, }) + if (testCase.requirements.length > 0) { + const properties = testcaseElement.ele('properties') + properties.ele('property', { + name: 'requirements', + value: testCase.requirements.join(",") + }) + } if (testCase.failure) { const failureElement = testcaseElement.ele(testCase.failure.kind) if (testCase.failure.kind === 'failure' && testCase.failure.type) { diff --git a/javascript/src/makeReport.ts b/javascript/src/makeReport.ts index ca97b5b..29e3ab4 100644 --- a/javascript/src/makeReport.ts +++ b/javascript/src/makeReport.ts @@ -15,6 +15,8 @@ const NAMING_STRATEGY = namingStrategy( NamingStrategyExampleName.NUMBER_AND_PICKLE_IF_PARAMETERIZED ) +const REQUIREMENT_TAG_PREFIX = "@Req:" + interface ReportSuite { time: number tests: number @@ -31,6 +33,7 @@ interface ReportTestCase { time: number failure?: ReportFailure output: string + requirements: string[] } interface ReportFailure { @@ -85,6 +88,9 @@ function makeTestCases(query: Query): ReadonlyArray { return formatStep(gherkinStep, pickleStep, testStepFinished.testStepResult.status) }) .join('\n'), + requirements: pickle.tags + .filter(tag => tag.name.startsWith(REQUIREMENT_TAG_PREFIX)) + .map(tag => tag.name.substring(REQUIREMENT_TAG_PREFIX.length)), } }) }