diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60efaa5..c2e04d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,7 @@ on: pull_request: branches: - main + - develop jobs: build: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1bb112b..503f364 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,7 +55,7 @@ jobs: java-version: '11' - name: Setup git credentials - uses: oleksiyrudenko/gha-git-credentials@v2.1.1 + uses: oleksiyrudenko/gha-git-credentials@v2-latest with: name: 'reportportal.io' email: 'support@reportportal.io' diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b91da1..a7d4792 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog ## [Unreleased] +### Changed +- Client version updated on [5.2.14](https://github.com/reportportal/client-java/releases/tag/5.2.14), by @HardNorth +- Called inner Features are now Nested Steps inside base Feature, by @HardNorth +- Unify Markdown description generation with other agents, by @HardNorth ## [5.0.5] ### Changed diff --git a/build.gradle b/build.gradle index 8049ffb..157426b 100644 --- a/build.gradle +++ b/build.gradle @@ -37,7 +37,6 @@ compileJava.options.encoding = 'UTF-8' compileTestJava.options.encoding = 'UTF-8' repositories { - mavenLocal() mavenCentral() } diff --git a/gradle.properties b/gradle.properties index 0a64bc4..3ded90b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,12 +1,12 @@ name=agent-java-karate -version=5.0.6-SNAPSHOT +version=5.1.0-SNAPSHOT description=EPAM ReportPortal. Karate test framework [1.3.1, ) adapter gradle_version=8.2 karate_version=1.4.1 junit_version=5.10.1 mockito_version=5.4.0 test_utils_version=0.0.3 -client_version=5.2.13 +client_version=5.2.14 slf4j_api_version=2.0.7 logger_version=5.2.2 hamcrest_version=2.2 diff --git a/src/main/java/com/epam/reportportal/karate/ReportPortalHook.java b/src/main/java/com/epam/reportportal/karate/ReportPortalHook.java index 6c51607..dc6570e 100644 --- a/src/main/java/com/epam/reportportal/karate/ReportPortalHook.java +++ b/src/main/java/com/epam/reportportal/karate/ReportPortalHook.java @@ -18,12 +18,14 @@ import com.epam.reportportal.karate.utils.BlockingConcurrentHashMap; import com.epam.reportportal.listeners.ItemStatus; +import com.epam.reportportal.listeners.ItemType; import com.epam.reportportal.listeners.ListenerParameters; import com.epam.reportportal.listeners.LogLevel; import com.epam.reportportal.service.Launch; import com.epam.reportportal.service.ReportPortal; import com.epam.reportportal.utils.MemoizingSupplier; import com.epam.reportportal.utils.StatusEvaluation; +import com.epam.reportportal.utils.markdown.MarkdownUtils; import com.epam.ta.reportportal.ws.model.FinishExecutionRQ; import com.epam.ta.reportportal.ws.model.FinishTestItemRQ; import com.epam.ta.reportportal.ws.model.StartTestItemRQ; @@ -34,15 +36,13 @@ import com.intuit.karate.http.HttpRequest; import com.intuit.karate.http.Response; import io.reactivex.Maybe; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.util.Calendar; -import java.util.Date; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; @@ -64,6 +64,7 @@ public class ReportPortalHook implements RuntimeHook { private final Map backgroundStatusMap = new ConcurrentHashMap<>(); private final Map> stepIdMap = new ConcurrentHashMap<>(); private final Map, Date> stepStartTimeMap = new ConcurrentHashMap<>(); + private final Set> innerFeatures = Collections.newSetFromMap(new ConcurrentHashMap<>()); private volatile Thread shutDownHook; /** @@ -72,9 +73,9 @@ public class ReportPortalHook implements RuntimeHook { * @param reportPortal the ReportPortal instance */ public ReportPortalHook(ReportPortal reportPortal) { + ListenerParameters params = reportPortal.getParameters(); + StartLaunchRQ rq = buildStartLaunchRq(params); launch = new MemoizingSupplier<>(() -> { - ListenerParameters params = reportPortal.getParameters(); - StartLaunchRQ rq = buildStartLaunchRq(params); Launch newLaunch = reportPortal.newLaunch(rq); //noinspection ReactiveStreamsUnusedPublisher newLaunch.start(); @@ -153,7 +154,7 @@ protected StartTestItemRQ buildStartFeatureRq(@Nonnull FeatureRuntime fr) { String parameters = String.format(PARAMETERS_PATTERN, formatParametersAsTable(getParameters(args))); String description = rq.getDescription(); if (isNotBlank(description)) { - rq.setDescription(String.format(MARKDOWN_DELIMITER_PATTERN, parameters, description)); + rq.setDescription(MarkdownUtils.asTwoParts(parameters, description)); } else { rq.setDescription(parameters); } @@ -163,9 +164,28 @@ protected StartTestItemRQ buildStartFeatureRq(@Nonnull FeatureRuntime fr) { @Override public boolean beforeFeature(FeatureRuntime fr) { + StartTestItemRQ rq = buildStartFeatureRq(fr); featureIdMap.computeIfAbsent(fr.featureCall.feature.getNameForReport(), - f -> new MemoizingSupplier<>(() -> launch.get().startTestItem(buildStartFeatureRq(fr))) - ); + f -> new MemoizingSupplier<>(() -> { + if(fr.caller == null || fr.caller.depth == 0) { + return launch.get().startTestItem(rq); + } else { + Maybe scenarioId = scenarioIdMap.get(fr.caller.parentRuntime.scenario.getUniqueId()); + if (scenarioId == null) { + LOGGER.error("ERROR: Trying to post unspecified scenario."); + return launch.get().startTestItem(rq); + } + rq.setType(ItemType.STEP.name()); + rq.setHasStats(false); + rq.setName(getInnerFeatureName(rq.getName())); + Maybe itemId = launch.get().startTestItem(scenarioId, rq); + innerFeatures.add(itemId); + if (StringUtils.isNotBlank(rq.getDescription())) { + ReportPortalUtils.sendLog(itemId, rq.getDescription(), LogLevel.INFO, rq.getStartTime()); + } + return itemId; + } + })); return true; } @@ -189,6 +209,7 @@ public void afterFeature(FeatureRuntime fr) { optionalId.ifPresent(featureId -> { //noinspection ReactiveStreamsUnusedPublisher launch.get().finishTestItem(featureId, buildFinishFeatureRq(fr)); + innerFeatures.remove(featureId); }); } @@ -200,18 +221,31 @@ public void afterFeature(FeatureRuntime fr) { */ @Nonnull protected StartTestItemRQ buildStartScenarioRq(@Nonnull ScenarioRuntime sr) { - return ReportPortalUtils.buildStartScenarioRq(sr.result); + StartTestItemRQ rq = ReportPortalUtils.buildStartScenarioRq(sr.result); + ofNullable(featureIdMap.get(sr.featureRuntime.featureCall.feature.getNameForReport())) + .map(Supplier::get) + .map(featureId -> innerFeatures.contains(featureId) ? featureId : null) + .ifPresent(featureId -> { + rq.setType(ItemType.STEP.name()); + rq.setHasStats(false); + rq.setName(getInnerScenarioName(rq.getName())); + }); + return rq; } @Override public boolean beforeScenario(ScenarioRuntime sr) { - Optional> optionalId = ofNullable(featureIdMap.get(sr.featureRuntime.featureCall.feature.getNameForReport())).map(Supplier::get); + StartTestItemRQ rq = buildStartScenarioRq(sr); + Optional> optionalId = ofNullable(featureIdMap.get(sr.featureRuntime.featureCall.feature.getNameForReport())) + .map(Supplier::get); if (optionalId.isEmpty()) { LOGGER.error("ERROR: Trying to post unspecified feature."); } optionalId.ifPresent(featureId -> { - StartTestItemRQ rq = buildStartScenarioRq(sr); Maybe scenarioId = launch.get().startTestItem(featureId, rq); + if (innerFeatures.contains(featureId) && StringUtils.isNotBlank(rq.getDescription())) { + ReportPortalUtils.sendLog(scenarioId, rq.getDescription(), LogLevel.INFO); + } scenarioIdMap.put(sr.scenario.getUniqueId(), scenarioId); }); return true; diff --git a/src/main/java/com/epam/reportportal/karate/ReportPortalUtils.java b/src/main/java/com/epam/reportportal/karate/ReportPortalUtils.java index a97231b..7ad60aa 100644 --- a/src/main/java/com/epam/reportportal/karate/ReportPortalUtils.java +++ b/src/main/java/com/epam/reportportal/karate/ReportPortalUtils.java @@ -25,6 +25,7 @@ import com.epam.reportportal.utils.AttributeParser; import com.epam.reportportal.utils.ParameterUtils; import com.epam.reportportal.utils.TestCaseIdUtils; +import com.epam.reportportal.utils.markdown.MarkdownUtils; import com.epam.reportportal.utils.properties.SystemAttributesExtractor; import com.epam.ta.reportportal.ws.model.FinishExecutionRQ; import com.epam.ta.reportportal.ws.model.FinishTestItemRQ; @@ -60,8 +61,10 @@ public class ReportPortalUtils { public static final String SKIPPED_ISSUE_KEY = "skippedIssue"; public static final String SCENARIO_CODE_REFERENCE_PATTERN = "%s/[SCENARIO:%s]"; public static final String EXAMPLE_CODE_REFERENCE_PATTERN = "%s/[EXAMPLE:%s%s]"; - public static final String MARKDOWN_DELIMITER = "\n\n---\n\n"; + public static final String MARKDOWN_DELIMITER = "\n" + MarkdownUtils.LOGICAL_SEPARATOR + "\n"; public static final String MARKDOWN_DELIMITER_PATTERN = "%s" + MARKDOWN_DELIMITER + "%s"; + public static final String FEATURE_TAG = "Feature: "; + public static final String SCENARIO_TAG = "Scenario: "; private static final Logger LOGGER = LoggerFactory.getLogger(ReportPortalUtils.class); private static final String PARAMETER_ITEMS_START = "["; private static final String PARAMETER_ITEMS_END = "]"; @@ -244,7 +247,7 @@ public static StartTestItemRQ buildStartFeatureRq(@Nonnull Feature feature) { String featurePath = feature.getResource().getUri().toString(); String description = feature.getDescription(); if (isNotBlank(description)) { - rq.setDescription(String.format(MARKDOWN_DELIMITER_PATTERN, featurePath, description)); + rq.setDescription(MarkdownUtils.asTwoParts(featurePath, description)); } else { rq.setDescription(featurePath); } @@ -319,13 +322,17 @@ public static StartTestItemRQ buildStartScenarioRq(@Nonnull ScenarioResult resul @Nonnull public static FinishTestItemRQ buildFinishScenarioRq(@Nonnull ScenarioResult result) { Scenario scenario = result.getScenario(); - FinishTestItemRQ rq = buildFinishTestItemRq(Calendar.getInstance().getTime(), result.getFailureMessageForDisplay() == null ? ItemStatus.PASSED : ItemStatus.FAILED); + FinishTestItemRQ rq = buildFinishTestItemRq( + Calendar.getInstance().getTime(), + result.getFailureMessageForDisplay() == null ? ItemStatus.PASSED : ItemStatus.FAILED + ); rq.setDescription(buildDescription(scenario, result.getErrorMessage(), getParameters(scenario))); return rq; } @Nonnull - private static String buildDescription(@Nonnull Scenario scenario, @Nullable String errorMessage, @Nullable List parameters) { + private static String buildDescription(@Nonnull Scenario scenario, @Nullable String errorMessage, + @Nullable List parameters) { StringBuilder descriptionBuilder = new StringBuilder(); if (parameters != null && !parameters.isEmpty()) { @@ -425,18 +432,30 @@ public static ItemStatus getStepStatus(String status) { * @param itemId item ID future * @param message log message to send * @param level log level + * @param logTime log time */ - public static void sendLog(Maybe itemId, String message, LogLevel level) { + public static void sendLog(Maybe itemId, String message, LogLevel level, Date logTime) { ReportPortal.emitLog(itemId, id -> { SaveLogRQ rq = new SaveLogRQ(); rq.setMessage(message); rq.setItemUuid(id); rq.setLevel(level.name()); - rq.setLogTime(Calendar.getInstance().getTime()); + rq.setLogTime(logTime); return rq; }); } + /** + * Send Step logs to ReportPortal. + * + * @param itemId item ID future + * @param message log message to send + * @param level log level + */ + public static void sendLog(Maybe itemId, String message, LogLevel level) { + sendLog(itemId, message, level, Calendar.getInstance().getTime()); + } + /** * Builds markdown representation of some code or script to be logged to ReportPortal * @@ -446,4 +465,24 @@ public static void sendLog(Maybe itemId, String message, LogLevel level) public static String asMarkdownCode(String code) { return String.format(MARKDOWN_CODE_PATTERN, code); } + + /** + * Build name of inner scenario (called by another scenario). + * + * @param name Scenario name + * @return Inner scenario name + */ + public static String getInnerScenarioName(String name) { + return SCENARIO_TAG + name; + } + + /** + * Build name of inner feature (called by another scenario). + * + * @param name Feature name + * @return Inner feature name + */ + public static String getInnerFeatureName(String name) { + return FEATURE_TAG + name; + } } diff --git a/src/main/java/com/epam/reportportal/karate/utils/BlockingConcurrentHashMap.java b/src/main/java/com/epam/reportportal/karate/utils/BlockingConcurrentHashMap.java index 7394261..f699e6d 100644 --- a/src/main/java/com/epam/reportportal/karate/utils/BlockingConcurrentHashMap.java +++ b/src/main/java/com/epam/reportportal/karate/utils/BlockingConcurrentHashMap.java @@ -28,6 +28,13 @@ import java.util.concurrent.TimeUnit; import java.util.function.Function; +/** + * This class ensures that {@link Map#computeIfAbsent(Object, Function)} is called only once for the same key. It has inner blocking timeout + * of 1 minute to wait for the value to be computed. + * + * @param a key type for the map + * @param a value type to store + */ public class BlockingConcurrentHashMap { private static final Logger LOGGER = LoggerFactory.getLogger(BlockingConcurrentHashMap.class); @@ -69,7 +76,7 @@ public T get(long timeout, TimeUnit unit) throws InterruptedException { private final Map> map = new ConcurrentHashMap<>(); - public void computeIfAbsent(@Nonnull K key, Function mappingFunction) { + public void computeIfAbsent(@Nonnull K key, Function mappingFunction) { map.computeIfAbsent(key, k -> new BlockingReference<>()).set(mappingFunction); } diff --git a/src/test/java/com/epam/reportportal/karate/description/CallWithParametersHookTest.java b/src/test/java/com/epam/reportportal/karate/description/CallWithParametersHookTest.java index 1fc96db..bf112e7 100644 --- a/src/test/java/com/epam/reportportal/karate/description/CallWithParametersHookTest.java +++ b/src/test/java/com/epam/reportportal/karate/description/CallWithParametersHookTest.java @@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -44,26 +45,28 @@ public class CallWithParametersHookTest { private static final String TEST_FEATURE = "classpath:feature/call.feature"; private static final String PARAMETERS_DESCRIPTION_PATTERN = - "Parameters:\n\n" + MarkdownUtils.TABLE_INDENT + "| vara | result |\n" + MarkdownUtils.TABLE_INDENT + "|------|--------|\n" - + MarkdownUtils.TABLE_INDENT + "|  2   |   4    |\n\n" + MarkdownUtils.TABLE_ROW_SEPARATOR; - private final List featureIds = Stream.generate(() -> CommonUtils.namedId("feature_")).limit(2).collect(Collectors.toList()); - private final List scenarioIds = Stream.generate(() -> CommonUtils.namedId("scenario_")).limit(2).collect(Collectors.toList()); - private final List stepIds = Stream.generate(() -> CommonUtils.namedId("step_")).limit(4).collect(Collectors.toList()); - private final List>>>> features = Stream.of( - Pair.of(featureIds.get(0), - (Collection>>) Collections.singletonList(Pair.of( - scenarioIds.get(0), - Collections.singletonList(stepIds.get(0)) - )) - ), - Pair.of( - featureIds.get(1), - (Collection>>) Collections.singletonList(Pair.of( - scenarioIds.get(1), - stepIds.subList(1, stepIds.size()) - )) - ) + "Parameters:\n\n" + MarkdownUtils.TABLE_INDENT + "|\u00A0vara\u00A0|\u00A0result\u00A0|\n" + MarkdownUtils.TABLE_INDENT + + "|------|--------|\n" + MarkdownUtils.TABLE_INDENT + + "|\u00A0\u00A02\u00A0\u00A0\u00A0|\u00A0\u00A0\u00A04\u00A0\u00A0\u00A0\u00A0|\n" + + MarkdownUtils.TABLE_ROW_SEPARATOR; + private final String featureId = CommonUtils.namedId("feature_"); + private final String scenarioId = CommonUtils.namedId("scenario_"); + private final String innerFeatureId = CommonUtils.namedId("feature_step_"); + private final List stepIds = Arrays.asList(CommonUtils.namedId("step_"), innerFeatureId); + private final String innerScenarioId = CommonUtils.namedId("scenario_step_"); + private final List innerStepIds = Stream.generate(() -> CommonUtils.namedId("inner_step_")) + .limit(3) + .collect(Collectors.toList()); + + private final List>>>> features = Stream.of(Pair.of(featureId, + (Collection>>) Collections.singletonList(Pair.of(scenarioId, stepIds)) + )) + .collect(Collectors.toList()); + private final List> nestedSteps = Stream.concat( + Stream.of(Pair.of(innerFeatureId, innerScenarioId)), + innerStepIds.stream().map(id -> Pair.of(innerScenarioId, id)) ).collect(Collectors.toList()); + private final ReportPortalClient client = mock(ReportPortalClient.class); private final ReportPortal rp = ReportPortal.create(client, standardParameters(), testExecutor()); @@ -71,6 +74,7 @@ public class CallWithParametersHookTest { public void setupMock() { mockLaunch(client, null); mockFeatures(client, features); + mockNestedSteps(client, nestedSteps); mockBatchLogging(client); } @@ -80,17 +84,19 @@ public void test_call_feature_with_parameters_hook_reporting() { assertThat(results.getFailCount(), equalTo(0)); ArgumentCaptor featureCaptor = ArgumentCaptor.forClass(StartTestItemRQ.class); - verify(client, times(2)).startTestItem(featureCaptor.capture()); + verify(client, times(1)).startTestItem(featureCaptor.capture()); ArgumentCaptor scenarioCaptor = ArgumentCaptor.forClass(StartTestItemRQ.class); - verify(client).startTestItem(same(featureIds.get(0)), scenarioCaptor.capture()); - verify(client).startTestItem(same(featureIds.get(1)), scenarioCaptor.capture()); + verify(client).startTestItem(same(featureId), scenarioCaptor.capture()); ArgumentCaptor stepCaptor = ArgumentCaptor.forClass(StartTestItemRQ.class); - verify(client).startTestItem(same(scenarioIds.get(0)), stepCaptor.capture()); - verify(client, times(3)).startTestItem(same(scenarioIds.get(1)), stepCaptor.capture()); + verify(client, times(2)).startTestItem(same(scenarioId), stepCaptor.capture()); + ArgumentCaptor innerScenarioCaptor = ArgumentCaptor.forClass(StartTestItemRQ.class); + verify(client).startTestItem(same(innerFeatureId), innerScenarioCaptor.capture()); + ArgumentCaptor innerStepCaptor = ArgumentCaptor.forClass(StartTestItemRQ.class); + verify(client, times(3)).startTestItem(same(innerScenarioId), innerStepCaptor.capture()); - StartTestItemRQ calledFeature = featureCaptor.getAllValues() + StartTestItemRQ calledFeature = stepCaptor.getAllValues() .stream() - .filter(rq -> "a feature which is called with parameters".equals(rq.getName())) + .filter(rq -> "Feature: a feature which is called with parameters".equals(rq.getName())) .findAny() .orElseThrow(); diff --git a/src/test/java/com/epam/reportportal/karate/description/ScenarioDescriptionErrorLogWithDescriptionAndExamplesTest.java b/src/test/java/com/epam/reportportal/karate/description/ScenarioDescriptionErrorLogWithDescriptionAndExamplesTest.java index a74d244..27bd500 100644 --- a/src/test/java/com/epam/reportportal/karate/description/ScenarioDescriptionErrorLogWithDescriptionAndExamplesTest.java +++ b/src/test/java/com/epam/reportportal/karate/description/ScenarioDescriptionErrorLogWithDescriptionAndExamplesTest.java @@ -48,7 +48,7 @@ public class ScenarioDescriptionErrorLogWithDescriptionAndExamplesTest { - public static final String MARKDOWN_DELIMITER_PATTERN_THREE_ARGS = "%s\n\n---\n\n%s\n\n---\n\n%s"; + public static final String MARKDOWN_DELIMITER_PATTERN_THREE_ARGS = "%s\n---\n%s\n---\n%s"; public static final String ERROR = "did not evaluate to 'true': mathResult == 5\nclasspath:feature/simple_failed_description_examples.feature:8"; public static final String ERROR_MESSAGE = "Then assert mathResult == 5\n" + ERROR; public static final String DESCRIPTION_ERROR_LOG = "Error:\n" + ERROR; diff --git a/src/test/java/com/epam/reportportal/karate/description/SimpleDescriptionTest.java b/src/test/java/com/epam/reportportal/karate/description/SimpleDescriptionTest.java index 2e621e7..613502c 100644 --- a/src/test/java/com/epam/reportportal/karate/description/SimpleDescriptionTest.java +++ b/src/test/java/com/epam/reportportal/karate/description/SimpleDescriptionTest.java @@ -78,7 +78,7 @@ public void test_description_for_all_possible_items(boolean report) { verify(client, times(3)).startTestItem(same(scenarioId), stepCaptor.capture()); StartTestItemRQ featureStart = featureCaptor.getValue(); - assertThat(featureStart.getDescription(), endsWith("feature/description.feature\n\n---\n\nThis is my Feature description.")); + assertThat(featureStart.getDescription(), endsWith("feature/description.feature\n---\nThis is my Feature description.")); StartTestItemRQ scenarioStart = scenarioCaptor.getValue(); assertThat(scenarioStart.getDescription(), equalTo(SCENARIO_DESCRIPTION)); diff --git a/src/test/java/com/epam/reportportal/karate/logging/HttpRequestLoggingTest.java b/src/test/java/com/epam/reportportal/karate/logging/HttpRequestLoggingTest.java index 7f2e67f..fa90a4a 100644 --- a/src/test/java/com/epam/reportportal/karate/logging/HttpRequestLoggingTest.java +++ b/src/test/java/com/epam/reportportal/karate/logging/HttpRequestLoggingTest.java @@ -64,7 +64,7 @@ public void test_http_request_logging(boolean report) { } else { results = TestUtils.runAsHook(rp, TEST_FEATURE); } - assertThat(results.getFailCount(), equalTo(0)); + assertThat(results.getFailCount(), equalTo(1)); ArgumentCaptor logCaptor = ArgumentCaptor.forClass(List.class); verify(client, atLeastOnce()).log(logCaptor.capture());