Skip to content

Commit fbd69d5

Browse files
authored
Fix attachment rendering (#12)
In Cucumber JVM attachments can be created in a few different ways. How influences whether the attachment ends up as elements of `embeddings` or `output`. | Method | Encoding | Media Type | Destination | | ------------------------------ | -------- | ------------------------- | ------------ | | `Scenario.attach(byte[], ...)` | BASE64 | any, from method | `embeddings` | | `Scenario.attach(String, ...)` | IDENTITY | any, from method | `embeddings` | | `Scenario.log(String)` | IDENTITY | text/x.cucumber.log+plain | `output` | Originally the implementation determined the destination based on the encoding, but this is obviously not correct. An alternative would be to select on the media type. This does come with the disadvantage that `Scenario.attach` also takes in a media type argument. So in theory that could be `text/x.cucumber.log+plain` but for now we'll assume that is not the case. Should this assumption prove wrong we could match on both encoding and media type, but it would be more reasonable for the user to change their code. Fixes: cucumber/cucumber-jvm#3069
1 parent e413e77 commit fbd69d5

File tree

3 files changed

+64
-14
lines changed

3 files changed

+64
-14
lines changed

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased]
9+
### Fixed
10+
- Fix attachment rendering ([#12](https://github.com/cucumber/cucumber-json-formatter/pull/7))
911

1012
## [0.1.2] - 2025-09-03
1113
### Fixed
12-
- `JvmArgument.val` and `JvmArgument.offset` are not required in practice ([#7](https://github.com/cucumber/cucumber-json-formatter/pull/7) M.P. Korstanje)
14+
- `JvmArgument.val` and `JvmArgument.offset` are not required in practice ([#7](https://github.com/cucumber/cucumber-json-formatter/pull/7))
1315

1416
## [0.1.1] - 2025-07-24
1517
### Fixed
1618
- Change artifact name to `cucumber-json-formatter`
1719

1820
## [0.1.0] - 2025-07-24
1921
### Added
20-
- Java implementation ([#1](https://github.com/cucumber/cucumber-json-formatter/pull/1) M.P. Korstanje)
22+
- Java implementation ([#1](https://github.com/cucumber/cucumber-json-formatter/pull/1))
2123

2224
[Unreleased]: https://github.com/cucumber/cucumber-json-formatter/compare/v0.1.2...HEAD
2325
[0.1.2]: https://github.com/cucumber/cucumber-json-formatter/compare/v0.1.1...v0.1.2

java/src/main/java/io/cucumber/jsonformatter/JsonReportWriter.java

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import io.cucumber.jsonformatter.CucumberJvmJson.JvmTag;
1818
import io.cucumber.messages.Convertor;
1919
import io.cucumber.messages.types.Attachment;
20+
import io.cucumber.messages.types.AttachmentContentEncoding;
2021
import io.cucumber.messages.types.Background;
2122
import io.cucumber.messages.types.DataTable;
2223
import io.cucumber.messages.types.DocString;
@@ -55,6 +56,7 @@
5556
import java.time.ZoneOffset;
5657
import java.time.format.DateTimeFormatter;
5758
import java.util.ArrayList;
59+
import java.util.Base64;
5860
import java.util.Collection;
5961
import java.util.Collections;
6062
import java.util.HashMap;
@@ -70,8 +72,7 @@
7072
import java.util.stream.Stream;
7173

7274
import static io.cucumber.jsonformatter.IdNamingVisitor.formatId;
73-
import static io.cucumber.messages.types.AttachmentContentEncoding.BASE64;
74-
import static io.cucumber.messages.types.AttachmentContentEncoding.IDENTITY;
75+
import static java.nio.charset.StandardCharsets.UTF_8;
7576
import static java.util.Collections.emptyList;
7677
import static java.util.Locale.ROOT;
7778
import static java.util.Objects.requireNonNull;
@@ -308,21 +309,59 @@ private JvmResult createJvmResult(TestStepResult result) {
308309

309310
private List<JvmEmbedding> createEmbeddings(List<Attachment> attachments) {
310311
return attachments.stream()
311-
.filter(attachment -> attachment.getContentEncoding() == BASE64)
312+
.filter(attachment -> !isTextCucumberLogPlain(attachment.getMediaType()))
312313
.map(attachment -> new JvmEmbedding(
313314
attachment.getMediaType(),
314-
attachment.getBody(),
315+
// Scenario.attach creates both plain and base64 attachments
316+
encodeBodyAsBase64(attachment),
315317
attachment.getFileName().orElse(null)))
316318
.collect(toList());
317319
}
318320

321+
private static String encodeBodyAsBase64(Attachment attachment) {
322+
AttachmentContentEncoding encoding = attachment.getContentEncoding();
323+
String body = attachment.getBody();
324+
switch (encoding) {
325+
case IDENTITY:
326+
byte[] bytes = body.getBytes(UTF_8);
327+
Base64.Encoder encoder = Base64.getEncoder();
328+
return encoder.encodeToString(bytes);
329+
case BASE64:
330+
return body;
331+
default:
332+
throw new RuntimeException("Unknown content encoding " + encoding);
333+
}
334+
}
335+
319336
private List<String> createOutput(List<Attachment> attachments) {
320337
return attachments.stream()
321-
.filter(attachment -> attachment.getContentEncoding() == IDENTITY)
322-
.map(Attachment::getBody)
338+
// Scenario.log creates text/x.cucumber.log+plain attachments
339+
// These are written as plain text output elements in the json report
340+
.filter(attachment -> isTextCucumberLogPlain(attachment.getMediaType()))
341+
// If someone snuck in a text/x.cucumber.log+plain through Scenario.attach(byte[])
342+
// we have to decode the body.
343+
.map(JsonReportWriter::encodeBodyAsPlainText)
323344
.collect(toList());
324345
}
325346

347+
private static String encodeBodyAsPlainText(Attachment attachment) {
348+
AttachmentContentEncoding encoding = attachment.getContentEncoding();
349+
String body = attachment.getBody();
350+
switch (encoding) {
351+
case IDENTITY:
352+
return body;
353+
case BASE64:
354+
Base64.Decoder decoder = Base64.getDecoder();
355+
return new String(decoder.decode(body), UTF_8);
356+
default:
357+
throw new RuntimeException("Unknown content encoding " + encoding);
358+
}
359+
}
360+
361+
private static boolean isTextCucumberLogPlain(String mediaType) {
362+
return mediaType.equals("text/x.cucumber.log+plain");
363+
}
364+
326365
private List<JvmLocationTag> createJvmLocationTags(Feature feature) {
327366
return feature.getTags().stream()
328367
.map(this::createJvmLocationTag)

testdata/compatibility-kit/attachments.ndjson.jvm.json

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,11 @@
3737
"duration": 1000000,
3838
"status": "passed"
3939
},
40-
"output": [
41-
"hello"
40+
"embeddings": [
41+
{
42+
"mime_type": "application/octet-stream",
43+
"data": "aGVsbG8="
44+
}
4245
]
4346
}
4447
]
@@ -131,8 +134,11 @@
131134
"line": 25,
132135
"value": "{\"message\": \"The <b>big</b> question\", \"foo\": \"bar\"}"
133136
},
134-
"output": [
135-
"{\"message\": \"The <b>big</b> question\", \"foo\": \"bar\"}"
137+
"embeddings": [
138+
{
139+
"mime_type": "application/json",
140+
"data": "eyJtZXNzYWdlIjogIlRoZSA8Yj5iaWc8L2I+IHF1ZXN0aW9uIiwgImZvbyI6ICJiYXIifQ=="
141+
}
136142
]
137143
}
138144
]
@@ -232,8 +238,11 @@
232238
"duration": 1000000,
233239
"status": "passed"
234240
},
235-
"output": [
236-
"https://cucumber.io"
241+
"embeddings": [
242+
{
243+
"mime_type": "text/uri-list",
244+
"data": "aHR0cHM6Ly9jdWN1bWJlci5pbw=="
245+
}
237246
]
238247
}
239248
]

0 commit comments

Comments
 (0)