diff --git a/.gitignore b/.gitignore index 1446b55..dcdd908 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,3 @@ replay_pid* # Properties file which can have sensitive information *.properties - diff --git a/README.adoc b/README.adoc index e20e8ca..b2b6c11 100644 --- a/README.adoc +++ b/README.adoc @@ -24,7 +24,7 @@ xray-test-env | No | Yes | @xray-test-env=DEV,TST The associations can be done either with a normal gherkin tag, a tabular parameter, or both: -By default a new execution will be created for however many groups of tests +By default, a new execution will be created for however many groups of tests are extracted from the feature file. This is elaborated in more detail in the below feature file example, but the short answer is you will have a number of executions equal to `[Distinct Test Plans] x [Distinct Test Environment Combinations]` @@ -148,6 +148,16 @@ xray.api.url=https://xray.cloud.getxray.app/api/v2/authenticate `XrayAuth.fromPropertiesFile("src/test/resources/xray.properties")` +[IMPORTANT] +==== +To avoid accidentally publishing secrets to Git, run the following command: + +[source,bash] +---- +git update-index --assume-unchanged src/test/resources/xray.properties +---- +==== + ==== Xray Updater This is the thing that actually monitors your test execution and can diff --git a/pom.xml b/pom.xml index a46824c..1f265a4 100644 --- a/pom.xml +++ b/pom.xml @@ -17,10 +17,10 @@ limitations under the License. 4.0.0 com.google.pdsl pdsl-xray - 5.1.2 + 5.2.0-SNAPSHOT jar xray_pdsl - http://maven.apache.org + https://maven.apache.org Plugin to integrate the XRAY test management system with the Polymorphic DSL test framework. @@ -52,13 +52,14 @@ limitations under the License. UTF-8 21 21 - 3.2.2 - 4.9.1 - 3.3.1 - 3.7.0 - 3.1.2 + 4.13.2 + 1.13.0 3.1.2 3.2.4 + 3.1.2 + 3.7.0 + 3.3.1 + 3.2.2 @@ -100,7 +101,7 @@ limitations under the License. com.google.pdsl pdsl - 1.11.1 + ${pdsl.version} org.antlr @@ -123,6 +124,11 @@ limitations under the License. plexus-cipher 1.7 + + com.google.guava + guava + 33.2.1-jre + @@ -136,7 +142,7 @@ limitations under the License. artifact-registry - artifactregistry://us-central1-maven.pkg.dev/gfp-p-artifacts-hub-01/ar-maven-experimental + artifactregistry://us-central1-maven.pkg.dev/gfp-p-artifacts-hub-01/ar-maven-experimental true @@ -224,6 +230,7 @@ limitations under the License. antlr4-maven-plugin ${antlr.version} + src/test/antlr4 -visitor diff --git a/src/main/antlr4/com/pdsl/grammars/AllGrammarsLexer.g4 b/src/main/antlr4/com/pdsl/grammars/AllGrammarsLexer.g4 deleted file mode 100644 index b1e7209..0000000 --- a/src/main/antlr4/com/pdsl/grammars/AllGrammarsLexer.g4 +++ /dev/null @@ -1,5 +0,0 @@ -lexer grammar AllGrammarsLexer; - -// This grammar is used by various tests that don't actually care what the input is - -ALL_INPUTS : .* ; \ No newline at end of file diff --git a/src/main/antlr4/com/pdsl/grammars/AllGrammarsParser.g4 b/src/main/antlr4/com/pdsl/grammars/AllGrammarsParser.g4 deleted file mode 100644 index 8de2cd3..0000000 --- a/src/main/antlr4/com/pdsl/grammars/AllGrammarsParser.g4 +++ /dev/null @@ -1,5 +0,0 @@ -parser grammar AllGrammarsParser; - -options {tokenVocab=AllGrammarsLexer; } - -polymorphicDslAllRules : ALL_INPUTS+; diff --git a/src/main/java/com/google/pdsl/xray/constants/StepStatus.java b/src/main/java/com/google/pdsl/xray/constants/StepStatus.java new file mode 100644 index 0000000..6d12faf --- /dev/null +++ b/src/main/java/com/google/pdsl/xray/constants/StepStatus.java @@ -0,0 +1,16 @@ +package com.google.pdsl.xray.constants; + +import java.util.Arrays; +import java.util.List; + +public enum StepStatus { + EXECUTING, + FAILED, + BLOCKED, + PASSED, + TODO; + + public static List getStringStatuses() { + return Arrays.stream(values()).map(Enum::toString).toList(); + } +} \ No newline at end of file diff --git a/src/main/java/com/google/pdsl/xray/constants/XrayTestTag.java b/src/main/java/com/google/pdsl/xray/constants/XrayTestTag.java new file mode 100644 index 0000000..0f0ce37 --- /dev/null +++ b/src/main/java/com/google/pdsl/xray/constants/XrayTestTag.java @@ -0,0 +1,45 @@ +package com.google.pdsl.xray.constants; + +/** + * XrayTags enum representing the supported Xray tags with their prefix values. + */ +public enum XrayTestTag { + + CASE("xray-test-case"), + ENV("xray-test-env"), + EXECUTION("xray-test-execution"), + PLAN("xray-test-plan"); + + // Defined locally within the enum + private static final String GHERKIN_TAG_PREFIX = "@"; + private static final String GHERKIN_EQUAL = "="; + + private final String value; + + XrayTestTag(String value) { + this.value = value; + } + + /** + * Gets the full Gherkin tag string including the prefix symbol. + * + * @return the complete tag string (e.g. "@xray-test-env=") + */ + public String getTagValue() { + return GHERKIN_TAG_PREFIX + value + GHERKIN_EQUAL; + } + + /** + * Wraps the tag value in angle brackets as an HTML or XML tag. + * + * @return the tag value wrapped in angle brackets (e.g., "<xray-test-case>") + */ + public String toHtmlTag() { + return "<" + value + ">"; + } + + @Override + public String toString() { + return value; + } +} diff --git a/src/main/java/com/google/pdsl/xray/core/XrayTestResultUpdater.java b/src/main/java/com/google/pdsl/xray/core/XrayTestResultUpdater.java index f6fe863..c2befff 100644 --- a/src/main/java/com/google/pdsl/xray/core/XrayTestResultUpdater.java +++ b/src/main/java/com/google/pdsl/xray/core/XrayTestResultUpdater.java @@ -3,18 +3,22 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Preconditions; +import com.google.pdsl.xray.constants.StepStatus; +import com.google.pdsl.xray.constants.XrayTestTag; import com.google.pdsl.xray.models.Info; import com.google.pdsl.xray.models.XrayTestExecution; import com.google.pdsl.xray.models.XrayTestResult; import com.pdsl.executors.ExecutorObserver; import com.pdsl.gherkin.GherkinObserver; import com.pdsl.gherkin.models.GherkinScenario; +import com.pdsl.gherkin.models.GherkinScenario.ScenarioPosition; import com.pdsl.reports.MetadataTestRunResults; import com.pdsl.reports.TestResult; import com.pdsl.testcases.SharedTestCase; import com.pdsl.testcases.TaggedTestCase; import com.pdsl.testcases.TestCase; import org.antlr.v4.runtime.tree.ParseTreeListener; +import org.antlr.v4.runtime.tree.ParseTreeVisitor; import org.apache.commons.codec.Charsets; import org.apache.http.HttpHeaders; import org.apache.http.HttpResponse; @@ -61,6 +65,8 @@ */ public class XrayTestResultUpdater implements GherkinObserver, ExecutorObserver { + private static final GherkinScenario.ScenarioPosition DEFAULT_POSITION = new GherkinScenario.ScenarioPosition(-1, -1, -1); + private final XrayAuth xrayAuth; private final ObjectMapper objectMapper; // Jackson ObjectMapper for JSON serialization private final Map testCaseXrayTestExecutionResultMap = new HashMap<>(); @@ -75,6 +81,7 @@ public class XrayTestResultUpdater implements GherkinObserver, ExecutorObserver private record TestPlan(String key, List testCases) { private record XrayTestCase(String key, Set environments, URI uri) { } + } private XrayTestResultUpdater(Builder builder) { @@ -104,7 +111,6 @@ private XrayTestResultUpdater(Builder builder) { this.objectMapper = builder.objectMapper; this.xrayStatuses = builder.xrayStatuses; } - public static class Builder { private static final ObjectMapper defaultObjectMapper = new ObjectMapper(); private Optional xrayAuth = Optional.empty(); @@ -115,7 +121,7 @@ public static class Builder { private String title; private Supplier> fieldSupplier; private Optional tempDirectory = Optional.empty(); - private List xrayStatuses = List.of("EXECUTING", "FAILED", "PASSED", "TODO"); + private List xrayStatuses = List.of("EXECUTING", "FAILED", "BLOCKED", "PASSED", "TODO"); public XrayTestResultUpdater build() { Preconditions.checkNotNull(fieldSupplier, "fieldSupplier must not be null"); @@ -176,11 +182,11 @@ public Builder withObjectMapper(ObjectMapper objectMapper) { this.objectMapper = objectMapper; return this; } - public Builder withXrayAuth(XrayAuth xrayAuth) { this.xrayAuth = Optional.ofNullable(xrayAuth); return this; } + } private void validateTempDirectory(Path tempDirectoryPath) { @@ -209,21 +215,20 @@ private void validateTempDirectory(Path tempDirectoryPath) { """, tempDirectoryPath.toUri())); } } - private record TestItem(String title, String testKey, String status, String testPlanKey, String testExecutionKey, Set environments, List stepDescription, Throwable throwable, Integer failedStepIndex) { + public Optional getFailedStepIndex() { return Optional.ofNullable(failedStepIndex); } - public Optional getThrowable() { return Optional.ofNullable(throwable); } - } + } private final class HierarchicalTestSuite { private record TestGroup(String source, int groupNumber, Map ordinals) { @@ -251,13 +256,14 @@ Collection toTestExecutionResults(List xrayStatuses) { } return results; } - } + } private record TestOrdinal(int ordinal, String xrayTestCase, List permutations) { } - private record TestPermutation(URI source, int permutationNumber, TestItem result) { } + private record TestPermutation(URI source, int permutationNumber, TestItem result) { } private final Map> source2TestGroups = new HashMap<>(); + void addTestResult(URI source, TestItem result, int groupNumber, int ordinal, int exampleNumber) { List testGroups = source2TestGroups.computeIfAbsent(source.getPath(), (k) -> new ArrayList<>()); TestGroup group = testGroups.stream() @@ -272,7 +278,6 @@ void addTestResult(URI source, TestItem result, int groupNumber, int ordinal, in List permutations = testOrdinal.permutations(); permutations.add(new TestPermutation(source, exampleNumber, result)); } - /** * Creates XrayTestExecutionResult objects based on any added test results in the past. * @return @@ -349,6 +354,7 @@ Collection info2Results() { } return results; } + } private static List iterationStepsFromPermutation(HierarchicalTestSuite.TestPermutation p) { @@ -361,10 +367,10 @@ private static List iterationStepsFromPermuta } return steps; } - /* Look at all the statuses we've gotten. Find the most significant status and use that to represent the overall status of the test. */ + private static String calculateOverallStatus(List statuses, List xrayStatuses) { int index = statuses.stream() .mapToInt(xrayStatuses::indexOf) @@ -385,10 +391,8 @@ private static String calculateOverallStatus(List statuses, List @Override public void onScenarioConverted(String title, List steps, Set tags, Map substitutions) { - addTags(tags, substitutions, ""); - addTags(tags, substitutions, ""); - addTags(tags, substitutions, ""); - addTags(tags, substitutions, ""); + Arrays.stream(XrayTestTag.values()) + .forEach(xrayTag -> addTags(tags, substitutions, xrayTag.toHtmlTag())); } private static void addTags(Set tags, Map substitutions, String key) { @@ -621,41 +625,22 @@ private String getXrayReportUrl() { * @param results The collection of test results. */ public void addResults(Collection results) { - for (TestResult result : results) { TestCase testCase = result.getTestCase(); if (testCase instanceof TaggedTestCase taggedTestCase) { - Set testPlanTags = new HashSet<>(extractTags(taggedTestCase.getTags(), "@xray-test-plan=")); - if (testPlanTags.size() > 1) { - throw new IllegalArgumentException(String.format(""" - Only one test plan can be associated with a test case! - Problem Test- - %s - %s - - Tags: %s - - """, testCase.getOriginalSource(), testCase.getTestTitle(), taggedTestCase.getTags())); - } - Set testExecutionTags = new HashSet<>(extractTags(taggedTestCase.getTags(), "@xray-test-execution=")); - if (testExecutionTags.size() > 1) { - throw new IllegalArgumentException(String.format(""" - Only one test execution can be associated with a test case! - Problem Test- - %s - %s - - Tags: %s - - """, testCase.getOriginalSource(), testCase.getTestTitle(), taggedTestCase.getTags())); - } - - Set envTags = extractTags(taggedTestCase.getTags(), "@xray-test-env=").stream() + Set testPlanTags = new HashSet<>(extractTags(taggedTestCase.getTags(), XrayTestTag.PLAN)); + verifyTestItemSize(testPlanTags, testCase, taggedTestCase, + "Only one test plan can be associated with a test case!"); + Set testExecutionTags = new HashSet<>(extractTags(taggedTestCase.getTags(), XrayTestTag.EXECUTION)); + verifyTestItemSize(testExecutionTags, testCase, taggedTestCase, + "Only one test execution can be associated with a test case!"); + + Set envTags = extractTags(taggedTestCase.getTags(), XrayTestTag.ENV).stream() .map(s -> Arrays.asList(s.split(","))) .flatMap(Collection::stream) .collect(Collectors.toUnmodifiableSet()); - Collection caseTags = extractTags(taggedTestCase.getTags(), "@xray-test-case="); + Collection caseTags = extractTags(taggedTestCase.getTags(), XrayTestTag.CASE); List testCases = caseTags.stream() .map(tc -> new TestPlan.XrayTestCase(tc, envTags, testCase.getOriginalSource())) @@ -675,21 +660,129 @@ public void addResults(Collection results) { )) .collect(Collectors.toSet()); HierarchicalTestSuite suite = testCaseXrayTestExecutionResultMap.computeIfAbsent(testPlan.key, (k) -> new HierarchicalTestSuite()); - GherkinScenario.ScenarioPosition position = getPosition(testCase.getOriginalSource()); - testItems.forEach(testItem -> { - suite.addTestResult( - testCase.getOriginalSource(), - testItem, - position.ruleIndex(), - position.ordinal(), - position.testIndex() - ); - }); + + testItems.forEach(testItem -> registerTestItem(testCase, testItem, suite)); + + // Extract step-level annotations from TestCase.STEP_COMMENTS + @SuppressWarnings("unchecked") + Map rawStepComments = (Map) testCase.getMetadata().get(TestCase.STEP_COMMENTS); + processStepComments(rawStepComments, testCase, result, testPlan, testExecutionTags, envTags, suite); + } + } + } + + private void verifyTestItemSize(Set testItems, TestCase testCase, TaggedTestCase taggedTestCase, + String message) { + if (testItems.size() > 1) { + throw new IllegalArgumentException(String.format(""" + %s + Problem Test- + %s + %s + + Tags: %s + + """, message, testCase.getOriginalSource(), testCase.getTestTitle(), taggedTestCase.getTags())); + } + } + + private void registerTestItem(TestCase testCase, XrayTestResultUpdater.TestItem testItem, + XrayTestResultUpdater.HierarchicalTestSuite suite) { + ScenarioPosition position = getPosition(testCase.getOriginalSource()); + suite.addTestResult( + testCase.getOriginalSource(), + testItem, + position.ruleIndex(), + position.ordinal(), + position.testIndex() + ); + } + + private void processStepComments(Map rawStepComments, TestCase testCase, TestResult result, + TestPlan testPlan, Set testExecutionTags, Set envTags, + HierarchicalTestSuite suite) { + if (rawStepComments == null || rawStepComments.isEmpty()) { + return; + } + List stepDescriptions = testCase.getUnfilteredPhraseBody(); + Integer failingIdx = result.getFailingPhrase().isPresent() ? result.getFailingPhrase().get().getPrefilteredIndex() : null; + + Map uniqueStepTestItems = new LinkedHashMap<>(); + + for (Map.Entry entry : rawStepComments.entrySet()) { + int stepIndex = entry.getKey(); + if (entry.getValue() instanceof Collection commentsList) { + for (Object commentObj : new HashSet<>(commentsList)) { + if (commentObj instanceof String comment) { + String normalizedComment = comment.trim(); + extractTagValue(normalizedComment, XrayTestTag.CASE) + .ifPresent(stepTestCaseKey -> { + StepStatus stepStatus = determineStepStatus(failingIdx, stepIndex); + TestItem stepTestItem = new TestItem( + testCase.getTestTitle(), + stepTestCaseKey, + stepStatus.name(), + testPlan.key, + testExecutionTags.stream().findFirst().orElse(null), + envTags.isEmpty() ? environments : envTags, + stepDescriptions, + (failingIdx != null && stepIndex == failingIdx) ? result.getFailureReason().orElse(null) : null, + failingIdx + ); + + uniqueStepTestItems.merge(stepTestCaseKey, stepTestItem, this::mergeStepTestItems); + }); + } } } } - private static final GherkinScenario.ScenarioPosition DEFAULT_POSITION = new GherkinScenario.ScenarioPosition(-1, -1, -1); + for (TestItem stepTestItem : uniqueStepTestItems.values()) { + registerTestItem(testCase, stepTestItem, suite); + } + } + + private TestItem mergeStepTestItems(TestItem existing, TestItem incoming) { + int existingPriority = xrayStatuses.indexOf(existing.status()); + int incomingPriority = xrayStatuses.indexOf(incoming.status()); + // Smaller index in xrayStatuses means more significant status (e.g. EXECUTING=0, FAILED=1, etc.) + if (incomingPriority >= 0 && (existingPriority < 0 || incomingPriority < existingPriority)) { + return incoming; + } + return existing; + } + + private StepStatus determineStepStatus(Integer failingStepIndex, int stepZeroBasedIdx) { + if (failingStepIndex == null || stepZeroBasedIdx < failingStepIndex) { + return StepStatus.PASSED; + } else if (stepZeroBasedIdx == failingStepIndex) { + return StepStatus.FAILED; + } else { + return StepStatus.BLOCKED; + } + } + + private static Collection extractTags(Collection tags, XrayTestTag xrayTag) { + return tags.stream() + .map(tag -> extractTagValue(tag, xrayTag)) + .filter(Optional::isPresent) + .map(Optional::get) + .map(Object::toString) + .collect(Collectors.toSet()); + } + + private static Optional extractTagValue(String rawText, XrayTestTag xrayTag) { + if (rawText != null && rawText.startsWith(xrayTag.getTagValue())) { + String[] split = rawText.split(xrayTag.getTagValue(), 2); + if (split.length == 2) { + return Optional.of(split[1]); + } else { + return Optional.empty(); + } + } + return Optional.empty(); + } + private GherkinScenario.ScenarioPosition getPosition(URI uri) { Map params = Arrays.stream(uri.getQuery().split("&")) @@ -698,30 +791,13 @@ private GherkinScenario.ScenarioPosition getPosition(URI uri) { try { return new GherkinScenario.ScenarioPosition(Integer.parseInt(params.get(GherkinScenario.ScenarioPosition.RULE_INDEX)), Integer.parseInt(params.get(GherkinScenario.ScenarioPosition.ORDINAL)), - Integer.parseInt(params.get(GherkinScenario.ScenarioPosition.TABLE_INDEX)) - ); + Integer.parseInt(params.get(GherkinScenario.ScenarioPosition.TABLE_INDEX)) + ); } catch (RuntimeException e) { return DEFAULT_POSITION; } } - private static List extractTags(Collection tags, String prefix) { - return tags.stream() - .filter(tag -> tag.toLowerCase().startsWith(prefix.toLowerCase())) - .map(tag -> { - String[] split = tag.split("=", 2); - if (split.length == 2) { - return Optional.of(split[1]); - } else { - return Optional.empty(); - } - }) - .filter(Optional::isPresent) - .map(Optional::get) - .map(Object::toString) - .toList(); - } - public Collection getXrayPayload() { return testCaseXrayTestExecutionResultMap.values().stream() .flatMap(s -> s.info2Results().stream()) @@ -741,9 +817,8 @@ public void onAfterTestSuite(Collection testCases, } @Override - public void onAfterTestSuite(Collection testCases, - org.antlr.v4.runtime.tree.ParseTreeVisitor visitor, MetadataTestRunResults results, - String context) { + public void onAfterTestSuite(Collection testCases, ParseTreeVisitor visitor, + MetadataTestRunResults results, String context) { addResults(results.getTestResults()); } } diff --git a/src/main/java/com/google/pdsl/xray/models/XrayTestResult.java b/src/main/java/com/google/pdsl/xray/models/XrayTestResult.java index 7bc38bd..aa0d5d2 100644 --- a/src/main/java/com/google/pdsl/xray/models/XrayTestResult.java +++ b/src/main/java/com/google/pdsl/xray/models/XrayTestResult.java @@ -15,6 +15,7 @@ limitations under the License. */ +import java.util.Collection; import java.util.List; import java.util.Map; @@ -27,7 +28,7 @@ * This object is intended to be serialized and follows the JSON schema used for the v2 * REST API. */ -public record XrayTestResult(String testKey, String status, List examples) { +public record XrayTestResult(String testKey, String status, Collection examples) { /** * Represents a single iteration for a "Manual" style test case in XRAY. *

diff --git a/src/test/java/com/google/pdsl/xray/XrayIntegrationTest.java b/src/test/java/com/google/pdsl/xray/XrayIntegrationTest.java index 21cd5e4..543da47 100644 --- a/src/test/java/com/google/pdsl/xray/XrayIntegrationTest.java +++ b/src/test/java/com/google/pdsl/xray/XrayIntegrationTest.java @@ -27,11 +27,15 @@ import java.io.IOException; import java.io.InputStream; import java.nio.file.Paths; -import java.util.*; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; import java.util.function.Supplier; import java.util.stream.Stream; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; /* Copyright 2025 Google LLC @@ -47,122 +51,124 @@ See the License for the specific language governing permissions and limitations under the License. */ + /** * This class contains JUnit Jupiter tests for Xray integration. It uses the Pdsl framework to * execute Gherkin scenarios and integrates with Xray for test management and reporting. */ public class XrayIntegrationTest { - private static final Properties properties = initProperties(); - - private static final XrayAuth xrayAuth = XrayAuth.fromPropertiesFile("src/test/resources/xray.properties"); - private static final XrayTestResultUpdater updater = new XrayTestResultUpdater.Builder( - "PDSL-XRAY Plugin E2E Tests", - """ - End to end tests for the pdsl-xray plugin. - These tests support the gherkin protocol both through special fields in - the examples table or tags directly above scenarios: - |xray-test-plan | xray-test-case | xray-test-env | - """, - () -> Map.of( - "fields", Map.of( - "project", Map.of("key", properties.get("xray.project.key")), - "summary", "Automated test run by Polymorphic DSL Test Framework", - "issuetype", Map.of("name", "Test Execution"), - "assignee", Map.of("accountId", properties.get("xray.reporter.accountId")), - "reporter", Map.of("accountId", properties.get("xray.reporter.accountId")) - ) - )).withXrayAuth(xrayAuth) - .build(); - - private static final DefaultPolymorphicDslTestExecutor traceableTestRunExecutor = new DefaultPolymorphicDslTestExecutor(); - private static final PolymorphicDslPhraseFilter MY_CUSTOM_PDSL_PHRASE_FILTER = new MyCustomPDSLPhraseFilter(); - private static final PickleJarFactory PICKLE_JAR_FACTORY = init(); - private static final Supplier parseTreeListenerSupplier = AllGrammarsParserBaseListener::new; - - private static Properties initProperties() { - Properties properties = new Properties(); - try { - properties.load(new FileInputStream("src/test/resources/xray.properties")); - return properties; - } catch (IOException e) { - throw new IllegalStateException(e); - } - } - - private static PickleJarFactory init() { - traceableTestRunExecutor.registerObserver(updater); - PickleJarFactory PICKLE_JAR_FACTORY = PickleJarFactory.getDefaultPickleJarFactory(); - PICKLE_JAR_FACTORY.registerObserver(updater); - return PICKLE_JAR_FACTORY; - } - - @TestTemplate - @ExtendWith(IosExtension.class) - public void iosTest(PdslExecutable executable) { - executable.execute(); - } - - @TestTemplate - @ExtendWith(AndroidExtension.class) - public void androidTest(PdslExecutable executable) { - executable.execute(); - } - - private static PdslConfigParameter createParameterWithTag(String tag) { - return PdslConfigParameter.createGherkinPdslConfig( - List.of( - new PdslTestParameter.Builder(parseTreeListenerSupplier, - AllGrammarsLexer.class, AllGrammarsParser.class) - .withTagExpression(tag) - .withIncludedResources(new String[]{"XRayIntegration.feature", "PdslXrayTabular.feature"}) - .build() + private static final Properties properties = initProperties(); + + private static final XrayAuth xrayAuth = XrayAuth.fromPropertiesFile("src/test/resources/xray.properties"); + private static final XrayTestResultUpdater updater = new XrayTestResultUpdater.Builder( + "PDSL-XRAY Plugin E2E Tests", + """ + End to end tests for the pdsl-xray plugin. + These tests support the gherkin protocol both through special fields in + the examples table or tags directly above scenarios: + |xray-test-plan | xray-test-case | xray-test-env | + """, + () -> Map.of( + "fields", Map.of( + "project", Map.of("key", properties.get("xray.project.key")), + "summary", "Automated test run by Polymorphic DSL Test Framework", + "issuetype", Map.of("name", "Test Execution"), + "assignee", Map.of("accountId", properties.get("xray.reporter.accountId")), + "reporter", Map.of("accountId", properties.get("xray.reporter.accountId")) ) - ) - .withApplicationName("Polymorphic DSL Framework") - .withContext("User Acceptance Test") - .withResourceRoot(Paths.get("src/test/resources/features").toUri()) - .withRecognizerRule("polymorphicDslAllRules") - .withTestRunExecutor(() -> traceableTestRunExecutor) - .withTestSpecificationFactoryGenerator( - () -> new DefaultGherkinTestSpecificationFactoryGenerator( - new DefaultGherkinTestSpecificationFactory.Builder((MY_CUSTOM_PDSL_PHRASE_FILTER)) - .withPickleJarFactory(PICKLE_JAR_FACTORY))) + )).withXrayAuth(xrayAuth) .build(); - } - /** - * A supplier that provides an instance of AllGrammarsParserBaseListener. - */ - private static class IosExtension extends PdslGherkinInvocationContextProvider { - - @Override - public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { - return getInvocationContext(createParameterWithTag("@ios")).stream(); + + private static final DefaultPolymorphicDslTestExecutor traceableTestRunExecutor = new DefaultPolymorphicDslTestExecutor(); + private static final PolymorphicDslPhraseFilter MY_CUSTOM_PDSL_PHRASE_FILTER = new MyCustomPDSLPhraseFilter(); + private static final PickleJarFactory PICKLE_JAR_FACTORY = init(); + private static final Supplier parseTreeListenerSupplier = AllGrammarsParserBaseListener::new; + + private static Properties initProperties() { + Properties properties = new Properties(); + try { + properties.load(new FileInputStream("src/test/resources/xray.properties")); + return properties; + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private static PickleJarFactory init() { + traceableTestRunExecutor.registerObserver(updater); + PickleJarFactory PICKLE_JAR_FACTORY = PickleJarFactory.getDefaultPickleJarFactory(); + PICKLE_JAR_FACTORY.registerObserver(updater); + return PICKLE_JAR_FACTORY; + } + + private static PdslConfigParameter createParameterWithTag(String tag) { + return PdslConfigParameter.createGherkinPdslConfig( + List.of( + new PdslTestParameter.Builder(parseTreeListenerSupplier, + AllGrammarsLexer.class, AllGrammarsParser.class) + .withTagExpression(tag) + .withIncludedResources(new String[]{"XRayIntegration.feature", "PdslXrayTabular.feature"}) + .build() + ) + ) + .withApplicationName("Polymorphic DSL Framework") + .withContext("User Acceptance Test") + .withResourceRoot(Paths.get("src/test/resources/features").toUri()) + .withRecognizerRule("polymorphicDslAllRules") + .withTestRunExecutor(() -> traceableTestRunExecutor) + .withTestSpecificationFactoryGenerator( + () -> new DefaultGherkinTestSpecificationFactoryGenerator( + new DefaultGherkinTestSpecificationFactory.Builder((MY_CUSTOM_PDSL_PHRASE_FILTER)) + .withPickleJarFactory(PICKLE_JAR_FACTORY))) + .build(); + } + + /** + * Publishes the test results to Xray after all tests have been executed. + */ + @AfterAll + public static void publishReportsToXray() { + // Validation: Check if the updater has created a valid Xray payload. + assertNotNull(updater.getXrayPayload(), "Xray payload is null."); + List responses = updater.publishReportsToXray(); + assertFalse(responses.isEmpty()); } - } - private static class AndroidExtension extends PdslGherkinInvocationContextProvider { - @Override - public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { - return getInvocationContext(createParameterWithTag("@wip")).stream(); + @TestTemplate + @ExtendWith(IosExtension.class) + public void iosTest(PdslExecutable executable) { + executable.execute(); } - } - - /** - * Publishes the test results to Xray after all tests have been executed. - */ - @AfterAll - public static void publishReportsToXray() { - // Validation: Check if the updater has created a valid Xray payload. - assertNotNull(updater.getXrayPayload(), "Xray payload is null."); - List responses = updater.publishReportsToXray(); - assertFalse(responses.isEmpty()); - } - - private static class MyCustomPDSLPhraseFilter implements PolymorphicDslPhraseFilter { - @Override - public Optional> filterPhrases(List testInput) { - return Optional.empty(); + + @TestTemplate + @ExtendWith(AndroidExtension.class) + public void androidTest(PdslExecutable executable) { + executable.execute(); + } + + /** + * A supplier that provides an instance of AllGrammarsParserBaseListener. + */ + private static class IosExtension extends PdslGherkinInvocationContextProvider { + + @Override + public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { + return getInvocationContext(createParameterWithTag("@ios")).stream(); + } + } + + private static class AndroidExtension extends PdslGherkinInvocationContextProvider { + @Override + public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { + return getInvocationContext(createParameterWithTag("@wip")).stream(); + } + } + + private static class MyCustomPDSLPhraseFilter implements PolymorphicDslPhraseFilter { + @Override + public Optional> filterPhrases(List testInput) { + return Optional.empty(); + } } - } } diff --git a/src/test/java/com/google/pdsl/xray/core/XrayTestResultUpdaterTest.java b/src/test/java/com/google/pdsl/xray/core/XrayTestResultUpdaterTest.java index a947886..c17dceb 100644 --- a/src/test/java/com/google/pdsl/xray/core/XrayTestResultUpdaterTest.java +++ b/src/test/java/com/google/pdsl/xray/core/XrayTestResultUpdaterTest.java @@ -1,21 +1,37 @@ package com.google.pdsl.xray.core; +import com.google.pdsl.xray.constants.StepStatus; +import com.google.pdsl.xray.models.XrayTestExecution; +import com.google.pdsl.xray.models.XrayTestResult; +import com.pdsl.reports.TestResult; +import com.pdsl.reports.proto.TechnicalReportData; +import com.pdsl.specifications.Phrase; +import com.pdsl.testcases.TaggedTestCase; +import com.pdsl.testcases.TestCase; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import java.io.IOException; +import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Properties; +import java.util.Set; import java.util.function.Supplier; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class XrayTestResultUpdaterTest { @@ -59,4 +75,130 @@ void build_withXrayAuthProperties_doesNotThrowException() throws IOException { assertDoesNotThrow(() -> xrayTestResultUpdaterBuilder.withPropertiesPath(propertiesFile).build()); } + + @Test + void addResults_withStepLevelComments_allPassed() { + XrayTestResultUpdater updater = xrayTestResultUpdaterBuilder.withXrayAuth(xrayAuth).build(); + + Map> stepComments = new HashMap<>(); + stepComments.put(1, List.of("@xray-test-case=STEP-KEY-1")); + stepComments.put(3, List.of("@xray-test-case=STEP-KEY-3")); + TaggedTestCase testCase = createMockTestCase(stepComments); + + // Mock TestResult + TestResult result = Mockito.mock(TestResult.class); + when(result.getTestCase()).thenReturn(testCase); + when(result.getStatus()).thenReturn(TechnicalReportData.Status.PASSED); + + updater.addResults(List.of(result)); + + XrayTestExecution testExecution = getTestExecution(updater, "EXEC-123"); + + assertEquals(3, testExecution.tests().size()); + assertEquals(StepStatus.PASSED.name(), getTestStatus(testExecution, "STEP-KEY-1")); + assertEquals(StepStatus.PASSED.name(), getTestStatus(testExecution, "STEP-KEY-3")); + assertEquals(StepStatus.PASSED.name(), getTestStatus(testExecution, "SCENARIO-KEY")); + } + + @Test + void addResults_withStepLevelComments_stepFailed() { + XrayTestResultUpdater updater = xrayTestResultUpdaterBuilder.withXrayAuth(xrayAuth).build(); + + // Setup step comments: + // Step 1 (index 1) has STEP-KEY-1 -> should be PASSED + // Step 2 (index 2) has STEP-KEY-2 -> should be FAILED + // Step 3 (index 3) has STEP-KEY-3 -> should be BLOCKED + Map> stepComments = new HashMap<>(); + stepComments.put(1, List.of("@xray-test-case=STEP-KEY-1")); + stepComments.put(2, List.of("@xray-test-case=STEP-KEY-2")); + stepComments.put(3, List.of("@xray-test-case=STEP-KEY-3")); + TaggedTestCase testCase = createMockTestCase(stepComments); + + Phrase failingPhrase = Mockito.mock(Phrase.class); + when(failingPhrase.getPrefilteredIndex()).thenReturn(2); + + // Mock TestResult + TestResult result = Mockito.mock(TestResult.class); + when(result.getTestCase()).thenReturn(testCase); + when(result.getStatus()).thenReturn(TechnicalReportData.Status.FAILED); + when(result.getFailureReason()).thenReturn(Optional.of(new RuntimeException("Test Failure"))); + when(result.getFailingPhrase()).thenReturn(Optional.of(failingPhrase)); + + updater.addResults(List.of(result)); + + XrayTestExecution testExecution = getTestExecution(updater, "EXEC-123"); + + // SCENARIO-KEY, STEP-KEY-1, STEP-KEY-2, STEP-KEY-3 + assertEquals(4, testExecution.tests().size()); + assertEquals(StepStatus.FAILED.name(), getTestStatus(testExecution, "SCENARIO-KEY")); + assertEquals(StepStatus.PASSED.name(), getTestStatus(testExecution, "STEP-KEY-1")); + assertEquals(StepStatus.FAILED.name(), getTestStatus(testExecution, "STEP-KEY-2")); + assertEquals(StepStatus.BLOCKED.name(), getTestStatus(testExecution, "STEP-KEY-3")); + } + + @Test + void addResults_withDuplicateStepLevelComments_avoidsDuplicatesAndConsolidatesStatus() { + XrayTestResultUpdater updater = xrayTestResultUpdaterBuilder.withXrayAuth(xrayAuth).build(); + + // given + // Step 1 (index 1) has STEP-KEY-1 -> should be PASSED + // Step 2 (index 2) has STEP-KEY-1 (duplicate) -> should be FAILED + // Step 3 (index 3) has STEP-KEY-1 (duplicate) -> should be BLOCKED + Map> stepComments = new HashMap<>(); + stepComments.put(1, List.of("@xray-test-case=STEP-KEY-1")); + stepComments.put(2, List.of("@xray-test-case=STEP-KEY-1")); + stepComments.put(3, List.of("@xray-test-case=STEP-KEY-1")); + TaggedTestCase testCase = createMockTestCase(stepComments); + + Phrase failingPhrase = Mockito.mock(Phrase.class); + when(failingPhrase.getPrefilteredIndex()).thenReturn(1); + + // when + TestResult result = Mockito.mock(TestResult.class); + when(result.getTestCase()).thenReturn(testCase); + when(result.getStatus()).thenReturn(TechnicalReportData.Status.FAILED); + when(result.getFailingPhrase()).thenReturn(Optional.of(failingPhrase)); + + updater.addResults(List.of(result)); + + XrayTestExecution testExecution = getTestExecution(updater, "EXEC-123"); + + // then + // Only 2 distinct tests: SCENARIO-KEY, STEP-KEY-1 (no duplicates) + assertEquals(2, testExecution.tests().size()); + assertEquals(StepStatus.FAILED.name(), getTestStatus(testExecution, "SCENARIO-KEY")); + // Overall status of STEP-KEY-1 should be FAILED since one of the steps failed + assertEquals(StepStatus.FAILED.name(), getTestStatus(testExecution, "STEP-KEY-1")); + } + + private TaggedTestCase createMockTestCase(Map> stepComments) { + TaggedTestCase testCase = Mockito.mock(TaggedTestCase.class); + when(testCase.getTags()).thenReturn(Set.of("@xray-test-plan=PLAN-123", "@xray-test-execution=EXEC-123", "@xray-test-case=SCENARIO-KEY")); + when(testCase.getOriginalSource()).thenReturn(URI.create("file:/some/path?ruleIndex=1&ordinal=2&tableIndex=3")); + when(testCase.getTestTitle()).thenReturn("My Scenario"); + when(testCase.getUnfilteredPhraseBody()).thenReturn(List.of("Given step one", "When step two", "Then step three")); + + Map metadata = new HashMap<>(); + metadata.put(TestCase.STEP_COMMENTS, stepComments); + when(testCase.getMetadata()).thenReturn(metadata); + return testCase; + } + + private XrayTestExecution getTestExecution(XrayTestResultUpdater updater, String testExecutionKey) { + + Collection payloads = updater.getXrayPayload().stream().toList(); + assertEquals(1, payloads.size()); + XrayTestExecution testExecution = payloads.iterator().next(); + assertEquals(testExecutionKey, testExecution.testExecutionKey()); + return testExecution; + } + + private String getTestStatus(XrayTestExecution testExecution, String testKey) { + XrayTestResult result = testExecution.tests().stream() + .filter(r -> r.testKey().equals(testKey)) + .findFirst() + .orElseThrow(() -> new AssertionError("Could not find test with key " + testKey)); + return result.status(); + } + } \ No newline at end of file diff --git a/src/test/resources/xray.properties b/src/test/resources/xray.properties index 0b3f93e..a5b1897 100644 --- a/src/test/resources/xray.properties +++ b/src/test/resources/xray.properties @@ -5,4 +5,3 @@ xray.api.report.url=https://xray.cloud.getxray.app/api/v2/import/execution/multi xray.environments=PRD xray.reporter.accountId= xray.project.key= -