From f85583eb22071ca95babd0e19ed3885610df2057 Mon Sep 17 00:00:00 2001 From: Artem Eroshenko Date: Tue, 12 Sep 2023 10:38:44 +0300 Subject: [PATCH] add carousel attachment feature (via #52) --- .github/workflows/build.yml | 9 +- build.gradle.kts | 16 +-- gradle.properties | 2 +- .../xcresults/carousel/Carousel.java | 20 ++++ .../xcresults/carousel/CarouselImage.java | 23 +++++ .../carousel/CarouselPostProcessor.java | 96 ++++++++++++++++++ .../xcresults/export/ExportCommand.java | 20 ++++ .../xcresults/export/ExportPostProcessor.java | 12 +++ .../xcresults/util/FreemarkerUtil.java | 51 ++++++++++ src/main/resources/.DS_Store | Bin 0 -> 6148 bytes .../freemarker/resource-config.json | 9 ++ .../native-image/resource-config.json | 9 ++ src/main/resources/templates/carousel.ftl | 45 ++++++++ 13 files changed, 299 insertions(+), 13 deletions(-) create mode 100644 src/main/java/io/eroshenkoam/xcresults/carousel/Carousel.java create mode 100644 src/main/java/io/eroshenkoam/xcresults/carousel/CarouselImage.java create mode 100644 src/main/java/io/eroshenkoam/xcresults/carousel/CarouselPostProcessor.java create mode 100644 src/main/java/io/eroshenkoam/xcresults/export/ExportPostProcessor.java create mode 100644 src/main/java/io/eroshenkoam/xcresults/util/FreemarkerUtil.java create mode 100644 src/main/resources/.DS_Store create mode 100644 src/main/resources/META-INF/native-image/org.freemarker/freemarker/resource-config.json create mode 100644 src/main/resources/META-INF/native-image/resource-config.json create mode 100644 src/main/resources/templates/carousel.ftl diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index caabc9c..d42807b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,10 +10,9 @@ jobs: runs-on: macos-latest steps: - uses: actions/checkout@v3 - - uses: actions/setup-java@v3 + - uses: graalvm/setup-graalvm@v1 with: - distribution: 'zulu' - java-version: '11' - cache: 'gradle' + java-version: '17' + distribution: 'graalvm' - run: ./gradlew build - - run: ./gradlew nativeImage + - run: ./gradlew nativeCompile diff --git a/build.gradle.kts b/build.gradle.kts index fc96ea3..518d8ee 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,6 @@ plugins { java - id("com.palantir.graal") version("0.12.0") + id("org.graalvm.buildtools.native") version "0.9.24" } group = "org.example" @@ -22,12 +22,13 @@ tasks.withType(JavaCompile::class) { options.encoding = "UTF-8" } -graal { - graalVersion("22.2.0") - javaVersion("11") - - mainClass("io.eroshenkoam.xcresults.XCResults") - outputName("xcresults") +graalvmNative { + toolchainDetection.set(true) + binaries { + named("main") { + mainClass.set("io.eroshenkoam.xcresults.XCResults") + } + } } repositories { @@ -39,6 +40,7 @@ dependencies { implementation("com.fasterxml.jackson.core:jackson-databind:2.10.2") implementation("io.qameta.allure:allure-model:2.13.1") + implementation("org.freemarker:freemarker:2.3.32") implementation("info.picocli:picocli:4.1.4") implementation("commons-io:commons-io:2.6") diff --git a/gradle.properties b/gradle.properties index a5a502d..58399a3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ version=3.18-SNAPSHOT # Graal VM -graalVersion=19.2.0 +graalVersion=23.0.1 \ No newline at end of file diff --git a/src/main/java/io/eroshenkoam/xcresults/carousel/Carousel.java b/src/main/java/io/eroshenkoam/xcresults/carousel/Carousel.java new file mode 100644 index 0000000..ea033ac --- /dev/null +++ b/src/main/java/io/eroshenkoam/xcresults/carousel/Carousel.java @@ -0,0 +1,20 @@ +package io.eroshenkoam.xcresults.carousel; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class Carousel implements Serializable { + + private final List images; + + public Carousel(final List images) { + this.images = Optional.ofNullable(images).orElseGet(ArrayList::new); + } + + public List getImages() { + return images; + } + +} diff --git a/src/main/java/io/eroshenkoam/xcresults/carousel/CarouselImage.java b/src/main/java/io/eroshenkoam/xcresults/carousel/CarouselImage.java new file mode 100644 index 0000000..877c7f9 --- /dev/null +++ b/src/main/java/io/eroshenkoam/xcresults/carousel/CarouselImage.java @@ -0,0 +1,23 @@ +package io.eroshenkoam.xcresults.carousel; + +import java.io.Serializable; + +public class CarouselImage implements Serializable { + + private final String name; + private final String base64; + + public CarouselImage(final String name, final String base64) { + this.name = name; + this.base64 = base64; + } + + public String getName() { + return name; + } + + public String getBase64() { + return base64; + } + +} diff --git a/src/main/java/io/eroshenkoam/xcresults/carousel/CarouselPostProcessor.java b/src/main/java/io/eroshenkoam/xcresults/carousel/CarouselPostProcessor.java new file mode 100644 index 0000000..6bd8d0b --- /dev/null +++ b/src/main/java/io/eroshenkoam/xcresults/carousel/CarouselPostProcessor.java @@ -0,0 +1,96 @@ +package io.eroshenkoam.xcresults.carousel; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import freemarker.template.TemplateException; +import io.eroshenkoam.xcresults.export.ExportPostProcessor; +import io.eroshenkoam.xcresults.util.FreemarkerUtil; +import io.qameta.allure.model.Attachment; +import io.qameta.allure.model.ExecutableItem; +import io.qameta.allure.model.TestResult; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static io.eroshenkoam.xcresults.util.FormatUtil.getAttachmentFileName; + +public class CarouselPostProcessor implements ExportPostProcessor { + + private final ObjectMapper mapper = new ObjectMapper() + .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); + + private final String templatePath; + + public CarouselPostProcessor(final String templatePath) { + this.templatePath = templatePath; + } + + @Override + public void processTestResults(final Path outputPath, final Map testResults) { + System.out.println("Carousel attachment feature enabled"); + Optional.ofNullable(templatePath) + .map(t -> String.format("Carousel template: %s", t)) + .ifPresent(System.out::println); + testResults.forEach((path, result) -> processTestResult(outputPath, path, result)); + } + + private void processTestResult(final Path outputPath, final Path path, final TestResult testResult) { + final List attachments = getAttachment(testResult, (a) -> a.getName().endsWith(".jpeg")); + final List carouselImages = attachments.stream() + .map(a -> convert(outputPath, a)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + if (carouselImages.size() == 0) { + return; + } + try { + final Carousel carousel = new Carousel(carouselImages); + final Map data = Map.of("carousel", carousel); + final String carouselContent = Objects.nonNull(templatePath) + ? FreemarkerUtil.render(Path.of(templatePath), data) + : FreemarkerUtil.render("templates/carousel.ftl", data); + final Path carouselPath = outputPath.resolve(getAttachmentFileName("html")); + Files.write(carouselPath, carouselContent.getBytes(StandardCharsets.UTF_8)); + testResult.getAttachments().add(new Attachment() + .setName("Carousel") + .setSource(carouselPath.getFileName().toString())); + mapper.writeValue(path.toFile(), testResult); + } catch (IOException | TemplateException e) { + System.out.println("Can not create carousel attachment: " + e.getMessage()); + } + } + + private List getAttachment(final ExecutableItem item, final Predicate filter) { + final List attachments = new ArrayList<>(); + item.getAttachments().stream() + .filter(filter) + .forEach(attachments::add); + if (Objects.nonNull(item.getSteps())) { + item.getSteps().forEach(step -> attachments.addAll(getAttachment(step, filter))); + } + return attachments; + } + + private Optional convert(final Path outputPath, final Attachment attachment) { + try { + final byte[] bytes = Files.readAllBytes(outputPath.resolve(attachment.getSource())); + final String content = "data:image/jpeg;base64, " + Base64.getEncoder().encodeToString(bytes); + return Optional.of(new CarouselImage(attachment.getName(), content)); + } catch (IOException e) { + return Optional.empty(); + } + + } + +} diff --git a/src/main/java/io/eroshenkoam/xcresults/export/ExportCommand.java b/src/main/java/io/eroshenkoam/xcresults/export/ExportCommand.java index 2b34325..c93a789 100644 --- a/src/main/java/io/eroshenkoam/xcresults/export/ExportCommand.java +++ b/src/main/java/io/eroshenkoam/xcresults/export/ExportCommand.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import io.eroshenkoam.xcresults.carousel.CarouselPostProcessor; import io.qameta.allure.model.ExecutableItem; import io.qameta.allure.model.TestResult; import org.apache.commons.io.FileUtils; @@ -70,6 +71,18 @@ public class ExportCommand implements Runnable { ) protected ExportFormat format = ExportFormat.allure2; + @CommandLine.Option( + names = {"--add-carousel-attachment"}, + description = "Add carousel attachment to test results" + ) + private Boolean addCarouselAttachment; + + @CommandLine.Option( + names = {"--carousel-template-path"}, + description = "Carousel attachment template path" + ) + private String carouselTemplatePath; + @CommandLine.Parameters( index = "0", description = "The directories with *.xcresults" @@ -134,6 +147,7 @@ private void runUnsafe() throws Exception { System.out.printf("Export information about %s test summaries...%n", testSummaries.size()); final Map attachmentsRefs = new HashMap<>(); + final Map testResults = new HashMap<>(); for (final Map.Entry entry : testSummaries.entrySet()) { final JsonNode testSummary = entry.getKey(); final ExportMeta meta = entry.getValue(); @@ -154,6 +168,7 @@ private void runUnsafe() throws Exception { } }); }); + testResults.put(testSummaryPath, testResult); } System.out.printf("Export information about %s attachments...%n", attachmentsRefs.size()); for (Map.Entry attachment : attachmentsRefs.entrySet()) { @@ -161,6 +176,11 @@ private void runUnsafe() throws Exception { final Path attachmentPath = outputPath.resolve(attachment.getKey()); exportReference(attachmentRef, attachmentPath); } + final List postProcessors = new ArrayList<>(); + if (Objects.nonNull(addCarouselAttachment)) { + postProcessors.add(new CarouselPostProcessor(carouselTemplatePath)); + } + postProcessors.forEach(postProcessor -> postProcessor.processTestResults(outputPath, testResults)); } private ExportMeta getTestMeta(final ExportMeta meta, final JsonNode testableSummary) { diff --git a/src/main/java/io/eroshenkoam/xcresults/export/ExportPostProcessor.java b/src/main/java/io/eroshenkoam/xcresults/export/ExportPostProcessor.java new file mode 100644 index 0000000..ebacb67 --- /dev/null +++ b/src/main/java/io/eroshenkoam/xcresults/export/ExportPostProcessor.java @@ -0,0 +1,12 @@ +package io.eroshenkoam.xcresults.export; + +import io.qameta.allure.model.TestResult; + +import java.nio.file.Path; +import java.util.Map; + +public interface ExportPostProcessor { + + void processTestResults(Path outputPath, Map testResults); + +} diff --git a/src/main/java/io/eroshenkoam/xcresults/util/FreemarkerUtil.java b/src/main/java/io/eroshenkoam/xcresults/util/FreemarkerUtil.java new file mode 100644 index 0000000..5c9db01 --- /dev/null +++ b/src/main/java/io/eroshenkoam/xcresults/util/FreemarkerUtil.java @@ -0,0 +1,51 @@ +package io.eroshenkoam.xcresults.util; + +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import freemarker.template.TemplateExceptionHandler; + +import java.io.IOException; +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +public class FreemarkerUtil { + + private FreemarkerUtil() { + } + + public static String render(final Path templatePath, final Map data) + throws IOException, TemplateException { + final Template template = new Template( + templatePath.getFileName().toString(), Files.readString(templatePath), getDefaultConfiguration() + ); + return render(template, data); + } + + public static String render(final String templateName, final Map data) + throws IOException, TemplateException { + return render(getDefaultConfiguration().getTemplate(templateName), data); + } + + public static Configuration getDefaultConfiguration() { + Configuration configuration = new Configuration(Configuration.VERSION_2_3_30); + configuration.setClassForTemplateLoading(FreemarkerUtil.class, "/"); + configuration.setDefaultEncoding("UTF-8"); + configuration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); + configuration.setLogTemplateExceptions(false); + configuration.setWrapUncheckedExceptions(true); + configuration.setFallbackOnNullLoopVariable(false); + return configuration; + } + + private static String render(final Template template, final Map data) + throws IOException, TemplateException { + try (StringWriter result = new StringWriter()) { + template.process(data, result); + return result.toString(); + } + } + +} diff --git a/src/main/resources/.DS_Store b/src/main/resources/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..a580bdfa4f58bbd6ddd40e9566b76e176687eb88 GIT binary patch literal 6148 zcmeHKIc~#13?vg54$`P}xnIZ+7J~BveZT<{xUpp;K5A8YSDxnKfgpnHCV-n@2#~YO z<*cA7L{UVvJ-xq(G$JyG8_Ji3soA;t#2zxEKsfH$$eSGHLk{O|QhhyP+$ZO3W&fN1 z*>86JI&QvBWtIw10V+TRr~noCg96rjVe{ueMk+uBsKAc`_I)UD!kdX>d zfw2OQv2CpXzrt_K|6>w&RDcTnD+P46?3OKFDSPYW<*e5h_zG?{-*7Xmor2))80hU7 g8*9f4FN(TiYn<1_A<*f_I~~ZM0n>#>1@5iD1$ + + + + + + + Carousel + + +
+
    + <#list 0..carousel.images?size-1 as i> + <@carouselItem carousel.images[i].name carousel.images[i].base64 i /> + +
+
+ + + +<#macro carouselItem name base64 index> +
  • + ${name} +
  • +