Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
/**
* Used to mark methods that produce attachments. Returned value of such methods
* will be copied and shown in the report as attachment.
*
* <p>Byte arrays are used as binary attachment content directly. Custom return types may implement
* {@link AttachmentBytes}; all other values are converted to their UTF-8 string representation.</p>
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2016-2026 Qameta Software Inc
*
* 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
*
* http://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 io.qameta.allure;

/**
* Provides binary attachment content for custom values returned by methods annotated with {@link Attachment}.
*
* <p>When an annotated method returns an implementation of this interface, the attachment aspect uses
* {@link #attachmentBytes()} instead of converting the returned object to a string.</p>
*/
@FunctionalInterface
public interface AttachmentBytes {

/**
* Returns the binary attachment content.
*
* @return the attachment content, must not be {@code null}
*/
byte[] attachmentBytes();
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import io.qameta.allure.Allure;
import io.qameta.allure.AllureLifecycle;
import io.qameta.allure.Attachment;
import io.qameta.allure.AttachmentBytes;
import io.qameta.allure.AttachmentOptions;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
Expand Down Expand Up @@ -57,7 +58,7 @@ public void anyMethod() {

/**
* Handles the attachment callback.
* If returned data is not a byte array, then use toString() method, and get bytes from it.
* If returned data is neither a byte array nor {@link AttachmentBytes}, then use its string representation.
*
* @param joinPoint the join point to process
* @param result the model object or framework result to process
Expand All @@ -76,10 +77,7 @@ public void attachment(final JoinPoint joinPoint, final Object result) {
final MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
final Attachment attachment = methodSignature.getMethod()
.getAnnotation(Attachment.class);
final byte[] bytes = (result instanceof byte[])
? (byte[]) result
: Objects.toString(result)
.getBytes(StandardCharsets.UTF_8);
final byte[] bytes = toBytes(result);

final String name = attachment.value().isEmpty()
? methodSignature.getName()
Expand All @@ -94,6 +92,16 @@ public void attachment(final JoinPoint joinPoint, final Object result) {
);
}

private static byte[] toBytes(final Object result) {
if (result instanceof byte[]) {
return (byte[]) result;
}
if (result instanceof AttachmentBytes) {
return ((AttachmentBytes) result).attachmentBytes();
}
return Objects.toString(result).getBytes(StandardCharsets.UTF_8);
}

/**
* Returns the lifecycle.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2016-2026 Qameta Software Inc
*
* 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
*
* http://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 io.qameta.allure.aspects;

import io.qameta.allure.Attachment;
import io.qameta.allure.AttachmentBytes;
import io.qameta.allure.Issue;
import io.qameta.allure.test.AllureResults;
import io.qameta.allure.test.IsolatedLifecycle;
import org.junit.jupiter.api.Test;

import static io.qameta.allure.test.RunUtils.runWithinTestContext;
import static org.assertj.core.api.Assertions.assertThat;

@SuppressWarnings("unused")
@IsolatedLifecycle
class AttachmentsAspectsTest {

@Issue("485")
@Test
void shouldUseBytesFromCustomAttachmentType() {
final byte[] expected = new byte[]{0, 1, 2, -1};

final AllureResults results = runWithinTestContext(() -> customAttachment(expected));

assertThat(results.getAttachmentsRecursively())
.singleElement()
.satisfies(attachment -> {
assertThat(attachment.getName()).isEqualTo("Custom attachment");
assertThat(attachment.getType()).isEqualTo("application/octet-stream");
assertThat(results.getAttachmentContent(attachment)).containsExactly(expected);
});
}

@Attachment(
value = "Custom attachment",
type = "application/octet-stream"
)
CustomAttachment customAttachment(final byte[] content) {
return new CustomAttachment(content);
}

static final class CustomAttachment implements AttachmentBytes {

private final byte[] content;

CustomAttachment(final byte[] content) {
this.content = content;
}

@Override
public byte[] attachmentBytes() {
return content;
}

@Override
public String toString() {
return "fallback content";
}
}
}
Loading