diff --git a/CHANGELOG.md b/CHANGELOG.md index e0e7bcf..7fa6e5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Fixed +- Fix attachment rendering ([#12](https://github.com/cucumber/cucumber-json-formatter/pull/7)) ## [0.1.2] - 2025-09-03 ### Fixed -- `JvmArgument.val` and `JvmArgument.offset` are not required in practice ([#7](https://github.com/cucumber/cucumber-json-formatter/pull/7) M.P. Korstanje) +- `JvmArgument.val` and `JvmArgument.offset` are not required in practice ([#7](https://github.com/cucumber/cucumber-json-formatter/pull/7)) ## [0.1.1] - 2025-07-24 ### Fixed @@ -17,7 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.0] - 2025-07-24 ### Added -- Java implementation ([#1](https://github.com/cucumber/cucumber-json-formatter/pull/1) M.P. Korstanje) +- Java implementation ([#1](https://github.com/cucumber/cucumber-json-formatter/pull/1)) [Unreleased]: https://github.com/cucumber/cucumber-json-formatter/compare/v0.1.2...HEAD [0.1.2]: https://github.com/cucumber/cucumber-json-formatter/compare/v0.1.1...v0.1.2 diff --git a/java/src/main/java/io/cucumber/jsonformatter/JsonReportWriter.java b/java/src/main/java/io/cucumber/jsonformatter/JsonReportWriter.java index 9eafc2c..151eeff 100644 --- a/java/src/main/java/io/cucumber/jsonformatter/JsonReportWriter.java +++ b/java/src/main/java/io/cucumber/jsonformatter/JsonReportWriter.java @@ -17,6 +17,7 @@ import io.cucumber.jsonformatter.CucumberJvmJson.JvmTag; import io.cucumber.messages.Convertor; import io.cucumber.messages.types.Attachment; +import io.cucumber.messages.types.AttachmentContentEncoding; import io.cucumber.messages.types.Background; import io.cucumber.messages.types.DataTable; import io.cucumber.messages.types.DocString; @@ -55,6 +56,7 @@ import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Base64; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -70,8 +72,7 @@ import java.util.stream.Stream; import static io.cucumber.jsonformatter.IdNamingVisitor.formatId; -import static io.cucumber.messages.types.AttachmentContentEncoding.BASE64; -import static io.cucumber.messages.types.AttachmentContentEncoding.IDENTITY; +import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Collections.emptyList; import static java.util.Locale.ROOT; import static java.util.Objects.requireNonNull; @@ -308,21 +309,59 @@ private JvmResult createJvmResult(TestStepResult result) { private List createEmbeddings(List attachments) { return attachments.stream() - .filter(attachment -> attachment.getContentEncoding() == BASE64) + .filter(attachment -> !isTextCucumberLogPlain(attachment.getMediaType())) .map(attachment -> new JvmEmbedding( attachment.getMediaType(), - attachment.getBody(), + // Scenario.attach creates both plain and base64 attachments + encodeBodyAsBase64(attachment), attachment.getFileName().orElse(null))) .collect(toList()); } + private static String encodeBodyAsBase64(Attachment attachment) { + AttachmentContentEncoding encoding = attachment.getContentEncoding(); + String body = attachment.getBody(); + switch (encoding) { + case IDENTITY: + byte[] bytes = body.getBytes(UTF_8); + Base64.Encoder encoder = Base64.getEncoder(); + return encoder.encodeToString(bytes); + case BASE64: + return body; + default: + throw new RuntimeException("Unknown content encoding " + encoding); + } + } + private List createOutput(List attachments) { return attachments.stream() - .filter(attachment -> attachment.getContentEncoding() == IDENTITY) - .map(Attachment::getBody) + // Scenario.log creates text/x.cucumber.log+plain attachments + // These are written as plain text output elements in the json report + .filter(attachment -> isTextCucumberLogPlain(attachment.getMediaType())) + // If someone snuck in a text/x.cucumber.log+plain through Scenario.attach(byte[]) + // we have to decode the body. + .map(JsonReportWriter::encodeBodyAsPlainText) .collect(toList()); } + private static String encodeBodyAsPlainText(Attachment attachment) { + AttachmentContentEncoding encoding = attachment.getContentEncoding(); + String body = attachment.getBody(); + switch (encoding) { + case IDENTITY: + return body; + case BASE64: + Base64.Decoder decoder = Base64.getDecoder(); + return new String(decoder.decode(body), UTF_8); + default: + throw new RuntimeException("Unknown content encoding " + encoding); + } + } + + private static boolean isTextCucumberLogPlain(String mediaType) { + return mediaType.equals("text/x.cucumber.log+plain"); + } + private List createJvmLocationTags(Feature feature) { return feature.getTags().stream() .map(this::createJvmLocationTag) diff --git a/testdata/compatibility-kit/attachments.ndjson.jvm.json b/testdata/compatibility-kit/attachments.ndjson.jvm.json index 02a853d..a124bb8 100644 --- a/testdata/compatibility-kit/attachments.ndjson.jvm.json +++ b/testdata/compatibility-kit/attachments.ndjson.jvm.json @@ -37,8 +37,11 @@ "duration": 1000000, "status": "passed" }, - "output": [ - "hello" + "embeddings": [ + { + "mime_type": "application/octet-stream", + "data": "aGVsbG8=" + } ] } ] @@ -131,8 +134,11 @@ "line": 25, "value": "{\"message\": \"The big question\", \"foo\": \"bar\"}" }, - "output": [ - "{\"message\": \"The big question\", \"foo\": \"bar\"}" + "embeddings": [ + { + "mime_type": "application/json", + "data": "eyJtZXNzYWdlIjogIlRoZSA8Yj5iaWc8L2I+IHF1ZXN0aW9uIiwgImZvbyI6ICJiYXIifQ==" + } ] } ] @@ -232,8 +238,11 @@ "duration": 1000000, "status": "passed" }, - "output": [ - "https://cucumber.io" + "embeddings": [ + { + "mime_type": "text/uri-list", + "data": "aHR0cHM6Ly9jdWN1bWJlci5pbw==" + } ] } ]