Skip to content

Commit

Permalink
Merge pull request #213 from reportportal/develop
Browse files Browse the repository at this point in the history
Release
  • Loading branch information
HardNorth authored Nov 13, 2024
2 parents 25a73e3 + 8e40365 commit 42e13a7
Show file tree
Hide file tree
Showing 11 changed files with 414 additions and 53 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Changelog

## [Unreleased]
### Added
- Common Stack Trace frames skip in description and logs, by @HardNorth
- `@Issue` and `@Issues` annotations support, by @HardNorth
### Changed
- Client version upgraded on [5.2.20](https://github.com/reportportal/client-java/releases/tag/5.2.20), by @HardNorth

## [5.4.3]
### Changed
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ repositories {
}

dependencies {
api 'com.epam.reportportal:client-java:5.2.15'
api 'com.epam.reportportal:client-java:5.2.20'

compileOnly "org.testng:testng:${testng_version}"
implementation 'org.slf4j:slf4j-api:2.0.4'
Expand Down
105 changes: 56 additions & 49 deletions src/main/java/com/epam/reportportal/testng/TestNGService.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@
*/
package com.epam.reportportal.testng;

import com.epam.reportportal.annotations.Description;
import com.epam.reportportal.annotations.DisplayName;
import com.epam.reportportal.annotations.ParameterKey;
import com.epam.reportportal.annotations.TestCaseId;
import com.epam.reportportal.annotations.*;
import com.epam.reportportal.annotations.attribute.Attributes;
import com.epam.reportportal.listeners.ItemStatus;
import com.epam.reportportal.listeners.ListenerParameters;
Expand All @@ -27,19 +24,15 @@
import com.epam.reportportal.service.item.TestCaseIdEntry;
import com.epam.reportportal.service.tree.TestItemTree;
import com.epam.reportportal.testng.util.internal.LimitedSizeConcurrentHashMap;
import com.epam.reportportal.utils.AttributeParser;
import com.epam.reportportal.utils.MemoizingSupplier;
import com.epam.reportportal.utils.ParameterUtils;
import com.epam.reportportal.utils.TestCaseIdUtils;
import com.epam.reportportal.utils.markdown.MarkdownUtils;
import com.epam.reportportal.utils.*;
import com.epam.reportportal.utils.formatting.MarkdownUtils;
import com.epam.reportportal.utils.properties.SystemAttributesExtractor;
import com.epam.ta.reportportal.ws.model.*;
import com.epam.ta.reportportal.ws.model.attribute.ItemAttributesRQ;
import com.epam.ta.reportportal.ws.model.launch.StartLaunchRQ;
import com.epam.ta.reportportal.ws.model.log.SaveLogRQ;
import io.reactivex.Maybe;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.testng.*;
import org.testng.annotations.Factory;
Expand All @@ -65,11 +58,11 @@
import java.util.stream.Stream;

import static com.epam.reportportal.testng.util.ItemTreeUtils.createKey;
import static com.epam.reportportal.utils.formatting.ExceptionUtils.getStackTrace;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toList;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.exception.ExceptionUtils.getStackTrace;

/**
* TestNG service implements operations for interaction ReportPortal
Expand Down Expand Up @@ -299,6 +292,21 @@ public void startConfiguration(ITestResult testResult) {
testResult.setAttribute(RP_ID, itemID);
}

/**
* Extension point to customize test step description
*
* @param testResult TestNG's testResult context
* @return Test/Step Description being sent to ReportPortal
*/
@Nonnull
protected String createStepDescription(@Nonnull ITestResult testResult) {
var methodDescriptionOptional = getMethodAnnotation(Description.class, testResult);
if (methodDescriptionOptional.isPresent()) {
return methodDescriptionOptional.get().value();
}
return testResult.getMethod().getDescription();
}

/**
* Extension point to customize test step creation event/request
*
Expand Down Expand Up @@ -364,6 +372,24 @@ public void startTestMethod(@Nonnull ITestResult testResult) {
}
}

/**
* Extension point to customize test step description with error message
*
* @param testResult TestNG's testResult context
* @return Test/Step Description being sent to ReportPortal
*/
@Nullable
private String getLogMessage(@Nonnull ITestResult testResult) {
String error = ofNullable(testResult.getThrowable()).map(t -> String.format(DESCRIPTION_ERROR_FORMAT,
getStackTrace(t, new Throwable())
)).orElse(null);
if (error == null) {
return null;
}
String description = createStepDescription(testResult);
return StringUtils.isNotBlank(description) ? MarkdownUtils.asTwoParts(description, error) : error;
}

/**
* Extension point to customize test method on it's finish
*
Expand Down Expand Up @@ -409,7 +435,7 @@ private void processFinishRetryFlag(ITestResult testResult, FinishTestItemRQ rq)
TestMethodType type = getAttribute(testResult, RP_METHOD_TYPE);

boolean isRetried = testResult.wasRetried();
if (TestMethodType.STEP == type && getAttribute(testResult, RP_RETRY) == null && isRetried) {
if (TestMethodType.STEP == type && getAttribute(testResult, RP_RETRY) == null && isRetried && rq.getIssue() == null) {
RETRY_STATUS_TRACKER.put(instance, Boolean.TRUE);
rq.setRetry(Boolean.TRUE);
rq.setIssue(Launch.NOT_ISSUE);
Expand Down Expand Up @@ -446,6 +472,15 @@ private void processFinishRetryFlag(ITestResult testResult, FinishTestItemRQ rq)
protected void createSkippedSteps(ITestResult testResult) {
}

@Nullable
protected com.epam.ta.reportportal.ws.model.issue.Issue createIssue(@Nonnull ITestResult testResult) {
String stepName = createStepName(testResult);
List<ParameterResource> parameters = createStepParameters(testResult);
return getMethodAnnotation(Issues.class, testResult).map(i -> IssueUtils.createIssue(i, stepName, parameters))
.orElseGet(() -> getMethodAnnotation(Issue.class, testResult).map(i -> IssueUtils.createIssue(i, stepName, parameters))
.orElse(null));
}

@Override
public void finishTestMethod(ItemStatus status, ITestResult testResult) {
Maybe<String> itemId = getAttribute(testResult, RP_ID);
Expand All @@ -463,14 +498,18 @@ public void finishTestMethod(ItemStatus status, ITestResult testResult) {
TestMethodType type = getAttribute(testResult, RP_METHOD_TYPE);
Object instance = testResult.getInstance();

if (type == TestMethodType.STEP) {
rq.setIssue(createIssue(testResult));
}

// TestNG does not repeat before methods if an after method fails during retries. But reports them as skipped.
// Mark before methods as not an issue if it is not a culprit.
if (instance != null) {
if (ItemStatus.FAILED == status && (TestMethodType.BEFORE_METHOD == type || TestMethodType.BEFORE_CLASS == type)) {
SKIPPED_STATUS_TRACKER.put(instance, Boolean.TRUE);
}
if (ItemStatus.SKIPPED == status && (SKIPPED_STATUS_TRACKER.containsKey(instance) || (TestMethodType.BEFORE_METHOD == type
&& getAttribute(testResult, RP_RETRY) != null))) {
if (status == ItemStatus.SKIPPED && rq.getIssue() == null
&& (SKIPPED_STATUS_TRACKER.containsKey(instance) || (TestMethodType.BEFORE_METHOD == type && getAttribute(testResult, RP_RETRY) != null))) {
rq.setIssue(Launch.NOT_ISSUE);
}
if (ItemStatus.SKIPPED == status && BEFORE_METHODS.contains(type) && testResult.getThrowable() != null) {
Expand All @@ -494,7 +533,7 @@ public void sendReportPortalMsg(final ITestResult result) {
rq.setItemUuid(itemUuid);
rq.setLevel("ERROR");
if (result.getThrowable() != null) {
rq.setMessage(getStackTrace(result.getThrowable()));
rq.setMessage(getStackTrace(result.getThrowable(), new Throwable()));
} else {
rq.setMessage("Test has failed without exception");
}
Expand Down Expand Up @@ -718,34 +757,15 @@ protected String createStepName(ITestResult testResult) {
return testStepName == null ? testResult.getMethod().getMethodName() : testStepName;
}

/**
* Extension point to customize test step description
*
* @param testResult TestNG's testResult context
* @return Test/Step Description being sent to ReportPortal
*/
protected String createStepDescription(ITestResult testResult) {
var methodDescriptionOptional = getMethodAnnotation(Description.class, testResult);
if (methodDescriptionOptional.isPresent()) {
return methodDescriptionOptional.get().value();
}
return testResult.getMethod().getDescription();
}

@Nullable
private TestCaseIdEntry getTestCaseId(@Nonnull String codeRef, @Nonnull ITestResult testResult) {
Method method = getMethod(testResult);
Object instance = testResult.getInstance();
List<Object> parameters = ofNullable(testResult.getParameters()).map(Arrays::asList).orElse(null);
TestCaseIdEntry id = getMethodAnnotation(TestCaseId.class,
testResult
).flatMap(a -> ofNullable(method).map(m -> TestCaseIdUtils.getTestCaseId(
a,
m,
codeRef,
parameters,
instance
))).orElse(TestCaseIdUtils.getTestCaseId(codeRef, parameters));
).flatMap(a -> ofNullable(method).map(m -> TestCaseIdUtils.getTestCaseId(a, m, codeRef, parameters, instance)))
.orElse(TestCaseIdUtils.getTestCaseId(codeRef, parameters));

return id == null ? null : id.getId().endsWith("[]") ? new TestCaseIdEntry(id.getId().substring(0, id.getId().length() - 2)) : id;
}
Expand Down Expand Up @@ -801,17 +821,4 @@ Maybe<String> getConfigParent(ITestResult testResult, TestMethodType type) {
}
return parentId;
}

/**
* Extension point to customize test step description with error message
*
* @param testResult TestNG's testResult context
* @return Test/Step Description being sent to ReportPortal
*/
private String getLogMessage(ITestResult testResult) {
String error = String.format(DESCRIPTION_ERROR_FORMAT, ExceptionUtils.getStackTrace(testResult.getThrowable())).trim();
return ofNullable(createStepDescription(testResult)).filter(StringUtils::isNotBlank)
.map(description -> MarkdownUtils.asTwoParts(description, error))
.orElse(error);
}
}
170 changes: 170 additions & 0 deletions src/test/java/com/epam/reportportal/testng/IssueReportingTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
* Copyright 2024 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.epam.reportportal.testng;

import com.epam.reportportal.listeners.ItemStatus;
import com.epam.reportportal.listeners.ListenerParameters;
import com.epam.reportportal.service.Launch;
import com.epam.reportportal.testng.integration.feature.issue.ParameterizedWithOneIssueTest;
import com.epam.reportportal.testng.integration.feature.issue.ParameterizedWithTwoIssueTest;
import com.epam.reportportal.testng.integration.feature.issue.SimpleIssueTest;
import com.epam.reportportal.testng.integration.feature.issue.SimpleTwoIssuesTest;
import com.epam.reportportal.testng.integration.util.TestUtils;
import com.epam.reportportal.util.test.CommonUtils;
import com.epam.ta.reportportal.ws.model.FinishTestItemRQ;
import com.epam.ta.reportportal.ws.model.OperationCompletionRS;
import com.epam.ta.reportportal.ws.model.issue.Issue;
import io.reactivex.Maybe;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.stubbing.Answer;

import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.Queue;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class IssueReportingTest {
public static class TestReportPortalListener extends BaseTestNGListener {
public static final ThreadLocal<Launch> LAUNCH_THREAD_LOCAL = new ThreadLocal<>();

public TestReportPortalListener() {
super(new TestNGService(LAUNCH_THREAD_LOCAL::get));
}

public static void initLaunch(Launch launch) {
LAUNCH_THREAD_LOCAL.set(launch);
}

public static Launch getLaunch() {
return LAUNCH_THREAD_LOCAL.get();
}
}

private final String suiteId = CommonUtils.namedId("suite_");
private final Maybe<String> suiteMaybe = Maybe.just(suiteId);
private final String stepOneId = CommonUtils.namedId("step_");
private final Maybe<String> stepOneMaybe = Maybe.just(stepOneId);
private final String stepTwoId = CommonUtils.namedId("step_");
private final Maybe<String> stepTwoMaybe = Maybe.just(stepTwoId);
private final String stepThreeId = CommonUtils.namedId("step_");
private final Maybe<String> stepThreeMaybe = Maybe.just(stepThreeId);
private final Queue<Maybe<String>> stepIds = new LinkedList<>(Arrays.asList(stepOneMaybe, stepTwoMaybe, stepThreeMaybe));

@Mock
private Launch launch;
@Mock
private ListenerParameters parameters;

@BeforeEach
public void initMocks() {
when(launch.getParameters()).thenReturn(parameters);
when(launch.startTestItem(any())).thenReturn(suiteMaybe);
when(launch.startTestItem(any(), any())).thenAnswer((Answer<Maybe<String>>) invocation -> CommonUtils.createMaybeUuid());
when(launch.startTestItem(same(suiteMaybe), any())).thenAnswer((Answer<Maybe<String>>) invocation -> stepIds.poll());
when(launch.startTestItem(same(stepOneMaybe), any())).thenAnswer((Answer<Maybe<String>>) invocation -> stepIds.poll());
when(launch.finishTestItem(any(),
any()
)).thenAnswer((Answer<Maybe<OperationCompletionRS>>) invocation -> Maybe.just(new OperationCompletionRS("OK")));
TestReportPortalListener.initLaunch(launch);
when(parameters.isCallbackReportingEnabled()).thenReturn(Boolean.TRUE);
}

@Test
public void verify_simple_test_failure() {
TestUtils.runTests(Collections.singletonList(TestReportPortalListener.class), SimpleIssueTest.class);

ArgumentCaptor<FinishTestItemRQ> testCaptor = ArgumentCaptor.forClass(FinishTestItemRQ.class);
verify(launch).finishTestItem(same(stepTwoMaybe), testCaptor.capture());

FinishTestItemRQ finishTestItemRQ = testCaptor.getValue();
assertThat(finishTestItemRQ.getStatus(), equalTo(ItemStatus.FAILED.name()));
assertThat(finishTestItemRQ.getIssue(), notNullValue());
Issue issue = finishTestItemRQ.getIssue();
assertThat(issue.getIssueType(), equalTo("pb001"));
assertThat(issue.getComment(), equalTo(SimpleIssueTest.FAILURE_MESSAGE));
}

@Test
public void verify_test_failure_with_two_issues() {
TestUtils.runTests(Collections.singletonList(TestReportPortalListener.class), SimpleTwoIssuesTest.class);

ArgumentCaptor<FinishTestItemRQ> testCaptor = ArgumentCaptor.forClass(FinishTestItemRQ.class);
verify(launch).finishTestItem(same(stepTwoMaybe), testCaptor.capture());

FinishTestItemRQ finishTestItemRQ = testCaptor.getValue();
assertThat(finishTestItemRQ.getStatus(), equalTo(ItemStatus.FAILED.name()));
assertThat(finishTestItemRQ.getIssue(), notNullValue());
Issue issue = finishTestItemRQ.getIssue();
assertThat(issue.getIssueType(), equalTo("ab001"));
assertThat(issue.getComment(), equalTo(SimpleTwoIssuesTest.FAILURE_MESSAGE));
}

@Test
public void verify_parameterized_test_failure_with_one_issue() {
TestUtils.runTests(Collections.singletonList(TestReportPortalListener.class), ParameterizedWithOneIssueTest.class);

ArgumentCaptor<FinishTestItemRQ> firstTestCaptor = ArgumentCaptor.forClass(FinishTestItemRQ.class);
verify(launch).finishTestItem(same(stepTwoMaybe), firstTestCaptor.capture());
ArgumentCaptor<FinishTestItemRQ> secondTestCaptor = ArgumentCaptor.forClass(FinishTestItemRQ.class);
verify(launch).finishTestItem(same(stepThreeMaybe), secondTestCaptor.capture());

FinishTestItemRQ finishTestItemRQ = firstTestCaptor.getValue();
assertThat(finishTestItemRQ.getStatus(), equalTo(ItemStatus.FAILED.name()));
assertThat(finishTestItemRQ.getIssue(), nullValue());

finishTestItemRQ = secondTestCaptor.getValue();
assertThat(finishTestItemRQ.getStatus(), equalTo(ItemStatus.FAILED.name()));
assertThat(finishTestItemRQ.getIssue(), notNullValue());
Issue issue = finishTestItemRQ.getIssue();
assertThat(issue.getIssueType(), equalTo("ab001"));
assertThat(issue.getComment(), equalTo(ParameterizedWithOneIssueTest.ISSUE_MESSAGE));
}

@Test
public void verify_parameterized_test_failure_with_two_issues() {
TestUtils.runTests(Collections.singletonList(TestReportPortalListener.class), ParameterizedWithTwoIssueTest.class);

ArgumentCaptor<FinishTestItemRQ> firstTestCaptor = ArgumentCaptor.forClass(FinishTestItemRQ.class);
verify(launch).finishTestItem(same(stepTwoMaybe), firstTestCaptor.capture());
ArgumentCaptor<FinishTestItemRQ> secondTestCaptor = ArgumentCaptor.forClass(FinishTestItemRQ.class);
verify(launch).finishTestItem(same(stepThreeMaybe), secondTestCaptor.capture());

FinishTestItemRQ finishTestItemRQ = firstTestCaptor.getValue();
assertThat(finishTestItemRQ.getStatus(), equalTo(ItemStatus.FAILED.name()));
assertThat(finishTestItemRQ.getIssue(), notNullValue());
Issue issue = finishTestItemRQ.getIssue();
assertThat(issue.getIssueType(), equalTo("ab001"));
assertThat(issue.getComment(), equalTo(ParameterizedWithTwoIssueTest.ISSUE_MESSAGE));

finishTestItemRQ = secondTestCaptor.getValue();
assertThat(finishTestItemRQ.getStatus(), equalTo(ItemStatus.FAILED.name()));
assertThat(finishTestItemRQ.getIssue(), notNullValue());
issue = finishTestItemRQ.getIssue();
assertThat(issue.getIssueType(), equalTo("pb001"));
assertThat(issue.getComment(), equalTo(ParameterizedWithTwoIssueTest.ISSUE_MESSAGE));
}
}
Loading

0 comments on commit 42e13a7

Please sign in to comment.