Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release #213

Merged
merged 8 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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