diff --git a/core/src/main/java/cz/xtf/core/openshift/OpenShift.java b/core/src/main/java/cz/xtf/core/openshift/OpenShift.java index 068bbdd9..bc71b599 100644 --- a/core/src/main/java/cz/xtf/core/openshift/OpenShift.java +++ b/core/src/main/java/cz/xtf/core/openshift/OpenShift.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.PrintStream; import java.io.Reader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -1316,11 +1317,21 @@ public Path storePodLog(Pod pod, Path dirPath, String fileName) throws IOExcepti return storeLog(log, dirPath, fileName); } + public void storePodLog(Pod pod, PrintStream printStream) { + String log = getPodLog(pod); + storeLog(log, printStream); + } + public Path storeBuildLog(Build build, Path dirPath, String fileName) throws IOException { String log = getBuildLog(build); return storeLog(log, dirPath, fileName); } + public void storeBuildLog(Build build, PrintStream printStream) { + String log = getBuildLog(build); + storeLog(log, printStream); + } + private Path storeLog(String log, Path dirPath, String fileName) throws IOException { Path filePath = dirPath.resolve(fileName); @@ -1331,6 +1342,10 @@ private Path storeLog(String log, Path dirPath, String fileName) throws IOExcept return filePath; } + private void storeLog(String log, PrintStream printStream) { + printStream.write(log.getBytes(), 0, log.length()); + } + // Waiting /** diff --git a/junit5/README.md b/junit5/README.md index 0f4e0a60..0b3b1f18 100644 --- a/junit5/README.md +++ b/junit5/README.md @@ -35,6 +35,7 @@ Extensions enable better test management. #### @OpenShiftRecorder Record OpenShift state when a test throws an exception or use `xtf.record.always` to record on success. Specify app names (which will be turned into regexes) to filter resources by name. When not specified, everything in test and build namespace will be recorded (regex - `.*`). Use `xtf.record.dir` to set the directory. +In case of test failure you can append pod logs and events into JUnit failure report by setting `xtf.record.append.logs.into.junit.report.on.failure` property. #### @CleanBeforeAll/@CleanBeforeEach Cleans namespace specified by `xtf.openshift.namespace` property. Either before all tests or each test execution. diff --git a/junit5/src/main/java/cz/xtf/junit5/config/JUnitConfig.java b/junit5/src/main/java/cz/xtf/junit5/config/JUnitConfig.java index 6bc04f81..9e71f26d 100644 --- a/junit5/src/main/java/cz/xtf/junit5/config/JUnitConfig.java +++ b/junit5/src/main/java/cz/xtf/junit5/config/JUnitConfig.java @@ -16,6 +16,7 @@ public class JUnitConfig { private static final String RECORD_DIR = "xtf.record.dir"; private static final String RECORD_ALWAYS = "xtf.record.always"; private static final String RECORD_BEFORE = "xtf.record.before"; + private static final String REPORT_LOGS_ON_FAILURE = "xtf.record.append.logs.into.junit.report.on.failure"; public static String recordDir() { return XTFConfig.get(RECORD_DIR); @@ -59,4 +60,9 @@ public static String jenkinsRerun() { public static boolean prebuilderSynchronized() { return Boolean.parseBoolean(XTFConfig.get(PREBUILDER_SYNCHRONIZED, "false")); } + + public static boolean appendLogsToReportOnFailure() { + return XTFConfig.get(REPORT_LOGS_ON_FAILURE) != null + && (XTFConfig.get(REPORT_LOGS_ON_FAILURE).equals("") || XTFConfig.get(REPORT_LOGS_ON_FAILURE).equals("true")); + } } diff --git a/junit5/src/main/java/cz/xtf/junit5/extensions/OpenShiftRecorderHandler.java b/junit5/src/main/java/cz/xtf/junit5/extensions/OpenShiftRecorderHandler.java index 2e9177d4..132e3d33 100644 --- a/junit5/src/main/java/cz/xtf/junit5/extensions/OpenShiftRecorderHandler.java +++ b/junit5/src/main/java/cz/xtf/junit5/extensions/OpenShiftRecorderHandler.java @@ -1,6 +1,10 @@ package cz.xtf.junit5.extensions; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.PrintStream; +import java.lang.reflect.Constructor; +import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; @@ -59,12 +63,25 @@ public void beforeEach(ExtensionContext context) { @Override public void handleTestExecutionException(ExtensionContext context, Throwable throwable) throws Throwable { + Throwable newThrowable = throwable; + final ByteArrayOutputStream messageStream = new ByteArrayOutputStream(); try { openShiftRecorderService.recordState(context); + + if (JUnitConfig.appendLogsToReportOnFailure()) { + try (PrintStream ps = new PrintStream(messageStream, true, StandardCharsets.UTF_8.name())) { + openShiftRecorderService.recordState(context, ps); + } + if (messageStream.size() != 0) { + Constructor constructor = throwable.getClass().getConstructor(String.class, + Throwable.class); + newThrowable = constructor.newInstance(throwable.getMessage() + messageStream, throwable.getCause()); + } + } } catch (Throwable t) { log.error("Throwable: ", t); } finally { - throw throwable; + throw newThrowable; } } diff --git a/junit5/src/main/java/cz/xtf/junit5/extensions/OpenShiftRecorderService.java b/junit5/src/main/java/cz/xtf/junit5/extensions/OpenShiftRecorderService.java index c925d9a6..b5392a47 100644 --- a/junit5/src/main/java/cz/xtf/junit5/extensions/OpenShiftRecorderService.java +++ b/junit5/src/main/java/cz/xtf/junit5/extensions/OpenShiftRecorderService.java @@ -1,6 +1,8 @@ package cz.xtf.junit5.extensions; import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintStream; import java.nio.file.Path; import java.nio.file.Paths; import java.util.concurrent.atomic.AtomicBoolean; @@ -133,7 +135,6 @@ public void initFilters(ExtensionContext context) { * @param context The test execution context */ public void updateFilters(ExtensionContext context) { - ExtensionContext.Store classStore = getClassStore(context); OpenShift master = OpenShifts.master(); OpenShift bm = BuildManagers.get().openShift(); if (!isFilterInitializationComplete(context)) { @@ -215,6 +216,15 @@ public void recordState(ExtensionContext context) throws IOException { !isMasterAndBuildNamespaceSame() ? getFilter(context, EVENT_FILTER_BUILDS) : null); } + public void recordState(ExtensionContext context, PrintStream ps) throws IOException { + savePodLogs(context, getFilter(context, POD_FILTER_MASTER), + !isMasterAndBuildNamespaceSame() ? getFilter(context, POD_FILTER_BUILDS) : null, ps); + saveBuildLogs(context, getFilter(context, BUILD_FILTER_MASTER), + !isMasterAndBuildNamespaceSame() ? getFilter(context, BUILD_FILTER_MASTER) : null, ps); + saveEvents(context, getFilter(context, EVENT_FILTER_MASTER), + !isMasterAndBuildNamespaceSame() ? getFilter(context, EVENT_FILTER_BUILDS) : null, ps); + } + private boolean isFilterInitializationComplete(ExtensionContext context) { ExtensionContext.Store classStore = getClassStore(context); return classStore.get(FILTER_INITIALIZATION_DONE, AtomicBoolean.class).get(); @@ -256,6 +266,10 @@ private void initMethodFilter(ExtensionContext context, String key, OpenShift op } private ExtensionContext.Store getClassStore(ExtensionContext extensionContext) { + if (extensionContext.getTestMethod().isPresent()) { + return extensionContext.getParent().get() + .getStore(ExtensionContext.Namespace.create(extensionContext.getRequiredTestClass())); + } return extensionContext.getStore(ExtensionContext.Namespace.create(extensionContext.getRequiredTestClass())); } @@ -447,6 +461,15 @@ protected void saveDCs(ExtensionContext context, ResourcesFilterBuilder masterFilter, ResourcesFilterBuilder buildsFilter) { + savePodLogs(context, masterFilter, buildsFilter, null); + } + + protected void savePodLogs(ExtensionContext context, ResourcesFilterBuilder masterFilter, + ResourcesFilterBuilder buildsFilter, PrintStream ps) { + if (ps != null) { + ps.println("\nAvailable PodLogs:"); + } + BiConsumer> podPrinter = (openShift, filter) -> openShift.getPods() .stream() .filter(filter.build()) @@ -460,10 +483,15 @@ protected void savePodLogs(ExtensionContext context, ResourcesFilterBuilder .count() == 0) .forEach(pod -> { try { - openShift.storePodLog( - pod, - Paths.get(attachmentsDir(), dirNameForTest(context)), - pod.getMetadata().getName() + ".log"); + if (ps == null) { + openShift.storePodLog( + pod, + Paths.get(attachmentsDir(), dirNameForTest(context)), + pod.getMetadata().getName() + ".log"); + } else { + ps.println("POD " + pod.getMetadata().getName() + ":"); + openShift.storePodLog(pod, ps); + } } catch (IOException e) { throw new RuntimeException(e); } @@ -477,10 +505,21 @@ protected void savePodLogs(ExtensionContext context, ResourcesFilterBuilder protected void saveEvents(ExtensionContext context, ResourcesFilterBuilder masterFilter, ResourcesFilterBuilder buildsFilter) throws IOException { + saveEvents(context, masterFilter, buildsFilter, null); + } + + protected void saveEvents(ExtensionContext context, ResourcesFilterBuilder masterFilter, + ResourcesFilterBuilder buildsFilter, PrintStream ps) throws IOException { // master namespace final Path eventsMasterLogPath = Paths.get(attachmentsDir(), dirNameForTest(context), "events-" + OpenShifts.master().getNamespace() + ".log"); - try (final ResourcesPrinterHelper printer = ResourcesPrinterHelper.forEvents(eventsMasterLogPath)) { + ResourcesPrinterHelper printHelper = ResourcesPrinterHelper.forEvents(eventsMasterLogPath); + if (ps != null) { + ps.println("\nAvailable Openshift events:"); + printHelper = ResourcesPrinterHelper.forEvents(new OutputStreamWriter(ps)); + } + + try (final ResourcesPrinterHelper printer = printHelper) { OpenShifts.master().getEvents() .stream() .filter(masterFilter.build()) @@ -490,7 +529,10 @@ protected void saveEvents(ExtensionContext context, ResourcesFilterBuilder printer = ResourcesPrinterHelper.forEvents(eventsBMLogPath)) { + printHelper = ps == null ? ResourcesPrinterHelper.forEvents(eventsBMLogPath) + : ResourcesPrinterHelper.forEvents(new OutputStreamWriter(ps)); + + try (final ResourcesPrinterHelper printer = printHelper) { BuildManagers.get().openShift().getEvents() .stream() .filter(buildsFilter.build()) @@ -501,17 +543,31 @@ protected void saveEvents(ExtensionContext context, ResourcesFilterBuilder masterFilter, ResourcesFilterBuilder buildsFilter) { + saveBuildLogs(context, masterFilter, buildsFilter, null); + } + + protected void saveBuildLogs(ExtensionContext context, ResourcesFilterBuilder masterFilter, + ResourcesFilterBuilder buildsFilter, PrintStream ps) { + if (ps != null) { + ps.println("\nAvailable BuildLogs:"); + } + BiConsumer> buildPrinter = (openShift, filter) -> openShift.getBuilds() .stream() .filter(filter.build()) .forEach(build -> { try { - openShift.storeBuildLog( - build, - Paths.get(attachmentsDir(), dirNameForTest(context)), - build.getMetadata().getName() + ".log"); - } catch (IOException e) { - throw new RuntimeException(e); + if (ps == null) { + openShift.storeBuildLog( + build, + Paths.get(attachmentsDir(), dirNameForTest(context)), + build.getMetadata().getName() + ".log"); + } else { + ps.println("BUILD " + build.getMetadata().getName() + ":"); + openShift.storeBuildLog(build, ps); + } + } catch (Exception e) { + // ignoring KubernetesClientExceptions } }); diff --git a/junit5/src/main/java/cz/xtf/junit5/extensions/helpers/ResourcesPrinterHelper.java b/junit5/src/main/java/cz/xtf/junit5/extensions/helpers/ResourcesPrinterHelper.java index d08f2cf4..85571231 100644 --- a/junit5/src/main/java/cz/xtf/junit5/extensions/helpers/ResourcesPrinterHelper.java +++ b/junit5/src/main/java/cz/xtf/junit5/extensions/helpers/ResourcesPrinterHelper.java @@ -1,10 +1,10 @@ package cz.xtf.junit5.extensions.helpers; -import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -25,9 +25,10 @@ import io.fabric8.openshift.api.model.Route; public class ResourcesPrinterHelper implements AutoCloseable { - private final Path file; + private Path file = null; private final Function> resourceToCols; private final List rows; + private OutputStreamWriter outputStream = null; private int[] maxLengths; private String[] headers = null; @@ -37,10 +38,20 @@ private ResourcesPrinterHelper(Path file, Function(); } + private ResourcesPrinterHelper(OutputStreamWriter outputStream, Function> resourceToCols) { + this.outputStream = outputStream; + this.resourceToCols = resourceToCols; + rows = new ArrayList<>(); + } + public static ResourcesPrinterHelper forEvents(Path filePath) { return new ResourcesPrinterHelper<>(filePath, ResourcesPrinterHelper::getEventCols); } + public static ResourcesPrinterHelper forEvents(OutputStreamWriter outputStream) { + return new ResourcesPrinterHelper<>(outputStream, ResourcesPrinterHelper::getEventCols); + } + public static ResourcesPrinterHelper forPods(Path filePath) { return new ResourcesPrinterHelper<>(filePath, ResourcesPrinterHelper::getPodCols); } @@ -222,9 +233,14 @@ public void close() throws IOException { } public void flush() throws IOException { - file.getParent().toFile().mkdirs(); + //mutually exclusive options file OR outputStream + OutputStreamWriter streamWriter = outputStream; + if (file != null) { + file.getParent().toFile().mkdirs(); + streamWriter = new OutputStreamWriter(Files.newOutputStream(file.toFile().toPath()), StandardCharsets.UTF_8); + } - try (final Writer writer = new OutputStreamWriter(new FileOutputStream(file.toFile()), StandardCharsets.UTF_8)) { + try (final Writer writer = streamWriter) { if (!rows.isEmpty()) { StringBuilder formatBuilder = new StringBuilder(); for (int maxLength : maxLengths) {