Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Changelog

## [Unreleased]
### Added
- Attachment logging, by @HardNorth

## [5.3.0]
### Changed
Expand Down
41 changes: 36 additions & 5 deletions src/main/java/com/epam/reportportal/karate/ReportPortalHook.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.epam.reportportal.utils.MemoizingSupplier;
import com.epam.reportportal.utils.StatusEvaluation;
import com.epam.reportportal.utils.formatting.MarkdownUtils;
import com.epam.reportportal.utils.reflect.Accessible;
import com.epam.ta.reportportal.ws.model.FinishExecutionRQ;
import com.epam.ta.reportportal.ws.model.FinishTestItemRQ;
import com.epam.ta.reportportal.ws.model.StartTestItemRQ;
Expand All @@ -44,10 +45,7 @@

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;

Expand Down Expand Up @@ -322,14 +320,44 @@ public void finishBackground(@Nullable StepResult stepResult, @Nonnull ScenarioR
}
}

/**
* Embed an attachment to ReportPortal.
*
* @param itemId item ID future
* @param embed Karate's Embed object
*/
protected void embedAttachment(@Nonnull Maybe<String> itemId, @Nonnull Embed embed) {
ReportPortalUtils.embedAttachment(itemId, embed);
}

/**
* Embed an attachment to ReportPortal.
*
* @param itemId item ID future
* @param embeddedEntities a list of Karate's Embed object
*/
protected void embedAttachments(@Nonnull Maybe<String> itemId, @Nullable List<Embed> embeddedEntities) {
ofNullable(embeddedEntities).ifPresent(embeds -> embeds.forEach(embed -> embedAttachment(itemId, embed)));
}

@Override
public void afterScenario(ScenarioRuntime sr) {
Maybe<String> scenarioId = scenarioIdMap.get(sr.scenario.getUniqueId());
finishBackground(null, sr);

if (scenarioId == null) {
LOGGER.error("ERROR: Trying to finish unspecified scenario.");
return;
}

try {
@SuppressWarnings("unchecked")
List<Embed> embeddedEntities = (List<Embed>) new Accessible(sr).field("embeds").getValue();
embedAttachments(scenarioId, embeddedEntities);
} catch (Exception e) {
LOGGER.warn("Unable to get scenario embeddings", e);
}

finishBackground(null, sr);
FinishTestItemRQ rq = buildFinishScenarioRq(sr);
//noinspection ReactiveStreamsUnusedPublisher
launch.get().finishTestItem(scenarioId, rq);
Expand Down Expand Up @@ -415,6 +443,9 @@ public void sendStepResults(StepResult stepResult, ScenarioRuntime sr) {
Maybe<String> stepId = stepIdMap.get(sr.scenario.getUniqueId());
Step step = stepResult.getStep();
Result result = stepResult.getResult();

ofNullable(stepResult.getEmbeds()).ifPresent(embeds -> embeds.forEach(embed -> embedAttachment(stepId, embed)));

if (result.isFailed()) {
String fullErrorMessage = step.getPrefix() + " " + step.getText();
String errorMessage = result.getErrorMessage();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,26 @@ protected void sendLog(Maybe<String> itemId, String message, LogLevel level) {
ReportPortalUtils.sendLog(itemId, message, level);
}

/**
* Embed an attachment to ReportPortal.
*
* @param itemId item ID future
* @param embed Karate's Embed object
*/
protected void embedAttachment(Maybe<String> itemId, Embed embed) {
ReportPortalUtils.embedAttachment(itemId, embed);
}

/**
* Embed an attachment to ReportPortal.
*
* @param itemId item ID future
* @param embeddedEntities a list of Karate's Embed object
*/
protected void embedAttachments(@Nonnull Maybe<String> itemId, @Nullable List<Embed> embeddedEntities) {
ofNullable(embeddedEntities).ifPresent(embeds -> embeds.forEach(embed -> embedAttachment(itemId, embed)));
}

/**
* Send Step execution results to ReportPortal.
*
Expand All @@ -412,6 +432,9 @@ public void sendStepResults(StepResult stepResult) {
if (isNotBlank(stepLog)) {
sendLog(stepId, stepLog, LogLevel.DEBUG);
}

embedAttachments(stepId, stepResult.getEmbeds());

if (result.isFailed()) {
String fullErrorMessage = step.getPrefix() + " " + step.getText();
String errorMessage = result.getErrorMessage();
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/com/epam/reportportal/karate/ReportPortalUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,30 @@ public static void sendLog(Maybe<String> itemId, String message, LogLevel level)
sendLog(itemId, message, level, Instant.now());
}

/**
* Embed an attachment to ReportPortal.
*
* @param itemId item ID future
* @param embed Karate's Embed object
*/
public static void embedAttachment(@Nonnull Maybe<String> itemId, @Nonnull Embed embed) {
ReportPortal.emitLog(itemId, id -> {
SaveLogRQ rq = new SaveLogRQ();
rq.setItemUuid(id);
rq.setLevel(LogLevel.INFO.name());
rq.setLogTime(Instant.now());
rq.setMessage("Attachment: " + embed.getResourceType().contentType);

SaveLogRQ.File file = new SaveLogRQ.File();
file.setName(embed.getFile().getName());
file.setContent(embed.getBytes());
file.setContentType(embed.getResourceType().contentType);
rq.setFile(file);

return rq;
});
}

/**
* Finish sending Launch data to ReportPortal.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright 2025 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.karate.logging;

import com.epam.reportportal.karate.utils.TestUtils;
import com.epam.reportportal.listeners.LogLevel;
import com.epam.reportportal.service.ReportPortal;
import com.epam.reportportal.service.ReportPortalClient;
import com.epam.reportportal.util.test.CommonUtils;
import com.epam.ta.reportportal.ws.model.log.SaveLogRQ;
import com.intuit.karate.Results;
import okhttp3.MultipartBody;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.epam.reportportal.karate.utils.TestUtils.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.*;

public class EmbedLoggingTest {
private static final String TEST_FEATURE = "classpath:feature/embed.feature";
private final String launchUuid = CommonUtils.namedId("launch_");
private final String featureId = CommonUtils.namedId("feature_");
private final String scenarioId = CommonUtils.namedId("scenario_");
private final List<String> stepIds = Stream.generate(() -> CommonUtils.namedId("step_")).limit(2).collect(Collectors.toList());

private final ReportPortalClient client = mock(ReportPortalClient.class);
private final ReportPortal rp = ReportPortal.create(client, standardParameters(), testExecutor());

@BeforeEach
public void setupMock() {
mockLaunch(client, launchUuid, featureId, scenarioId, stepIds);
mockBatchLogging(client);
}

@ParameterizedTest
@ValueSource(booleans = { true, false })
@SuppressWarnings({ "unchecked", "rawtypes" })
public void test_embed_image_attachment(boolean report) {
Results results;
if (report) {
results = TestUtils.runAsReport(rp, TEST_FEATURE);
} else {
results = TestUtils.runAsHook(rp, TEST_FEATURE);
}
assertThat(results.getFailCount(), equalTo(0));

ArgumentCaptor<List> logCaptor = ArgumentCaptor.forClass(List.class);
verify(client).log(logCaptor.capture());

List<SaveLogRQ> logs = logCaptor.getAllValues()
.stream()
.flatMap(rq -> extractJsonParts((List<MultipartBody.Part>) rq).stream())
.collect(Collectors.toList());
assertThat(logs, hasSize(2));

List<SaveLogRQ> attachmentLogs = logs.stream()
.filter(log -> log.getFile() != null)
.filter(log -> log.getMessage() != null && log.getMessage().startsWith("Attachment: "))
.collect(Collectors.toList());

assertThat("Should have one attachment log message", attachmentLogs, hasSize(1));

// Verify the attachment log properties
SaveLogRQ attachmentLog = attachmentLogs.get(0);
assertThat("Attachment log should have INFO level", attachmentLog.getLevel(), equalTo(LogLevel.INFO.name()));
assertThat("Attachment log should have item UUID", attachmentLog.getItemUuid(), notNullValue());
assertThat("Attachment log should have log time", attachmentLog.getLogTime(), notNullValue());
assertThat("Attachment message should contain image/png", attachmentLog.getMessage(), equalTo("Attachment: image/png"));

List<Pair<String, byte[]>> attachments = logCaptor.getAllValues()
.stream()
.flatMap(rq -> extractBinaryParts((List<MultipartBody.Part>) rq).stream())
.collect(Collectors.toList());
assertThat(attachments, hasSize(1));
assertThat(attachments.get(0).getKey(), equalTo("image/png"));
assertThat(attachments.get(0).getValue().length, greaterThan(0));
}
}

18 changes: 18 additions & 0 deletions src/test/java/com/epam/reportportal/karate/utils/TestUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import io.reactivex.Maybe;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okio.Buffer;
import org.apache.commons.lang3.tuple.Pair;
Expand Down Expand Up @@ -232,4 +233,21 @@ public static List<SaveLogRQ> extractJsonParts(List<MultipartBody.Part> parts) {
.flatMap(Collection::stream)
.collect(Collectors.toList());
}

public static List<Pair<String, byte[]>> extractBinaryParts(List<MultipartBody.Part> parts) {
return parts.stream()
.filter(p -> ofNullable(p.headers()).map(headers -> headers.get("Content-Disposition"))
.map(h -> h.contains(Constants.LOG_REQUEST_BINARY_PART))
.orElse(false))
.map(MultipartBody.Part::body)
.map(b -> {
Buffer buf = new Buffer();
try {
b.writeTo(buf);
} catch (IOException ignore) {
}
return Pair.of(ofNullable(b.contentType()).map(MediaType::toString).orElse(null), buf.readByteArray());
})
.collect(Collectors.toList());
}
}
5 changes: 5 additions & 0 deletions src/test/resources/feature/embed.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Feature: Demonstrate image attachment

Scenario: I attach image to report
When def bytes = karate.read('classpath:pug/lucky.png')
Then karate.embed(bytes, 'image/png')
Binary file added src/test/resources/pug/lucky.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/test/resources/pug/unlucky.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.