diff --git a/test-frame-log-collector/README.md b/test-frame-log-collector/README.md index da3d56f..a637707 100644 --- a/test-frame-log-collector/README.md +++ b/test-frame-log-collector/README.md @@ -169,6 +169,43 @@ the logs path will then look like this: ``` The tree path will look similarly to above examples, there will be folders for Namespaces matching the specified labels. +### Global log collector +`CollectLogs` is an annotation which can handle collecting logs which user want automatically in case of test failure or before/after failure. +It gets configuration passed into `GlobalLogCollector` and call collecting in proper callbacks. + +Register `GlobalLogCollector` handlers and configure + +```java +import io.skodjob.testframe.LogCollectorBuilder; +import io.skodjob.testframe.annotations.CollectLogs; +import io.skodjob.testframe.listeners.GlobalLogCollector; +import io.skodjob.testframe.resources.KubeResourceManager; +import org.junit.jupiter.api.Test; + +@CollectLogs +class TestClass() { + static { + // Setup global log collector and handlers + GlobalLogCollector.setupGlobalLogCollector(new LogCollectorBuilder() + .withNamespacedResources("sa", "deployment", "configmaps", "secret") + .withClusterWideResources("nodes") + .withKubeClient(KubeResourceManager.getKubeClient()) + .withKubeCmdClient(KubeResourceManager.getKubeCmdClient()) + .withRootFolderPath("/some-path/path/") + .build()); + GlobalLogCollector.addLogCallback(() -> { + GlobalLogCollector.getGlobalLogCollector().collectFromNamespaces("test-namespace", "test-namespace-2"); + GlobalLogCollector.getGlobalLogCollector().collectClusterWideResources(); + }); + } + + @Test + void test() { + ... + } +} +``` + ### Specifying additional folder path In case that you would like to collect the logs to additional sub-directories of your root folder, the `LogCollector` contains diff --git a/test-frame-log-collector/src/main/java/io/skodjob/testframe/annotations/CollectLogs.java b/test-frame-log-collector/src/main/java/io/skodjob/testframe/annotations/CollectLogs.java new file mode 100644 index 0000000..adcda8b --- /dev/null +++ b/test-frame-log-collector/src/main/java/io/skodjob/testframe/annotations/CollectLogs.java @@ -0,0 +1,29 @@ +/* + * Copyright Skodjob authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +package io.skodjob.testframe.annotations; + +import io.skodjob.testframe.listeners.GlobalLogCollector; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * This annotation is used to automatically call log collecting + * when test of prepare and post phase fails in JUnit tests. + * It is applied at the class level. + *

+ * It uses the {@link GlobalLogCollector} + */ +@Target(ElementType.TYPE) +@Retention(RUNTIME) +@Inherited +@ExtendWith(GlobalLogCollector.class) +public @interface CollectLogs { +} diff --git a/test-frame-log-collector/src/main/java/io/skodjob/testframe/listeners/GlobalLogCollector.java b/test-frame-log-collector/src/main/java/io/skodjob/testframe/listeners/GlobalLogCollector.java new file mode 100644 index 0000000..0ada058 --- /dev/null +++ b/test-frame-log-collector/src/main/java/io/skodjob/testframe/listeners/GlobalLogCollector.java @@ -0,0 +1,140 @@ +/* + * Copyright Skodjob authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +package io.skodjob.testframe.listeners; + +import io.skodjob.testframe.LogCollector; +import io.skodjob.testframe.annotations.CollectLogs; +import io.skodjob.testframe.interfaces.ThrowableRunner; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler; +import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; + +import java.util.LinkedList; +import java.util.List; + +/** + * Represents global log collector which is automatically called on text fail or error even in setup or post methods + */ +public class GlobalLogCollector implements TestExecutionExceptionHandler, LifecycleMethodExecutionExceptionHandler { + private static final Logger LOGGER = LogManager.getLogger(GlobalLogCollector.class); + private static LogCollector globalInstance; + private static final List COLLECT_CALLBACKS = new LinkedList<>(); + + /** + * Private constructor + */ + private GlobalLogCollector() { + // empty constructor + } + + /** + * Handler when test fails + * + * @param extensionContext extension context + * @param throwable throwable + * @throws Throwable original throwable + */ + @Override + public void handleTestExecutionException(ExtensionContext extensionContext, Throwable throwable) throws Throwable { + saveKubeState(); + throw throwable; + } + + /** + * Handles beforeAll exception + * + * @param context extensionContext + * @param throwable throwable + * @throws Throwable original throwable + */ + @Override + public void handleBeforeAllMethodExecutionException(ExtensionContext context, Throwable throwable)throws Throwable { + saveKubeState(); + LifecycleMethodExecutionExceptionHandler.super.handleBeforeAllMethodExecutionException(context, throwable); + } + + /** + * Handles beforeEach exception + * + * @param context extensionContext + * @param throwable throwable + * @throws Throwable original throwable + */ + @Override + public void handleBeforeEachMethodExecutionException(ExtensionContext context, + Throwable throwable) throws Throwable { + saveKubeState(); + LifecycleMethodExecutionExceptionHandler.super.handleBeforeEachMethodExecutionException(context, throwable); + } + + /** + * Handles afterEach exception + * + * @param context extensionContext + * @param throwable throwable + * @throws Throwable original throwable + */ + @Override + public void handleAfterEachMethodExecutionException(ExtensionContext context, + Throwable throwable) throws Throwable { + saveKubeState(); + LifecycleMethodExecutionExceptionHandler.super.handleAfterEachMethodExecutionException(context, throwable); + } + + /** + * Handles afterAll exception + * + * @param context extensionContext + * @param throwable throwable + * @throws Throwable original throwable + */ + @Override + public void handleAfterAllMethodExecutionException(ExtensionContext context, Throwable throwable) throws Throwable { + saveKubeState(); + LifecycleMethodExecutionExceptionHandler.super.handleAfterAllMethodExecutionException(context, throwable); + } + + /** + * Setup globalLogCollector which is automatically used within {@link CollectLogs} annotation + * + * @param globalLogCollector log collector instance + */ + public static void setupGlobalLogCollector(LogCollector globalLogCollector) { + globalInstance = globalLogCollector; + } + + /** + * Returns globalLogCollector instance + * + * @return global log collector instance + */ + public static LogCollector getGlobalLogCollector() { + if (globalInstance == null) { + throw new NullPointerException("Global log collector is not initialized"); + } + return globalInstance; + } + + /** + * Adds callback for running log collecting + * + * @param callback callback method with log collecting + */ + public static void addLogCallback(ThrowableRunner callback) { + COLLECT_CALLBACKS.add(callback); + } + + private void saveKubeState() { + try { + for (ThrowableRunner runner : COLLECT_CALLBACKS) { + runner.run(); + } + } catch (Exception ex) { + LOGGER.error("Cannot collect all data", ex); + } + } +} diff --git a/test-frame-test-examples/src/test/java/io/skodjob/testframe/test/integration/AbstractIT.java b/test-frame-test-examples/src/test/java/io/skodjob/testframe/test/integration/AbstractIT.java index a0607f8..155e94f 100644 --- a/test-frame-test-examples/src/test/java/io/skodjob/testframe/test/integration/AbstractIT.java +++ b/test-frame-test-examples/src/test/java/io/skodjob/testframe/test/integration/AbstractIT.java @@ -4,6 +4,10 @@ */ package io.skodjob.testframe.test.integration; +import io.skodjob.testframe.LogCollectorBuilder; +import io.skodjob.testframe.annotations.CollectLogs; +import io.skodjob.testframe.listeners.GlobalLogCollector; +import io.skodjob.testframe.test.integration.helpers.GlobalLogCollectorTestHandler; import io.skodjob.testframe.utils.LoggerUtils; import io.skodjob.testframe.annotations.ResourceManager; import io.skodjob.testframe.annotations.TestVisualSeparator; @@ -12,33 +16,61 @@ import io.skodjob.testframe.resources.NamespaceType; import io.skodjob.testframe.resources.ServiceAccountType; import io.skodjob.testframe.utils.KubeUtils; +import org.junit.jupiter.api.extension.ExtendWith; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.concurrent.atomic.AtomicBoolean; +@ExtendWith(GlobalLogCollectorTestHandler.class) // For testing purpose @ResourceManager +@CollectLogs @TestVisualSeparator public abstract class AbstractIT { static AtomicBoolean isCreateHandlerCalled = new AtomicBoolean(false); static AtomicBoolean isDeleteHandlerCalled = new AtomicBoolean(false); + public static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm"); + public static final Path LOG_DIR = Paths.get(System.getProperty("user.dir"), "target", "logs") + .resolve("test-run-" + DATE_FORMAT.format(LocalDateTime.now())); static { + // Register resources which KRM uses for handling instead of native status check KubeResourceManager.getInstance().setResourceTypes( new NamespaceType(), new ServiceAccountType(), new DeploymentType() ); + + // Register callback which are called with every create resource method for every resource KubeResourceManager.getInstance().addCreateCallback(r -> { isCreateHandlerCalled.set(true); if (r.getKind().equals("Namespace")) { KubeUtils.labelNamespace(r.getMetadata().getName(), "test-label", "true"); } }); + + // Register callback which are called with every delete resource method for every resource KubeResourceManager.getInstance().addDeleteCallback(r -> { isDeleteHandlerCalled.set(true); if (r.getKind().equals("Namespace")) { LoggerUtils.logResource("Deleted", r); } }); + + // Setup global log collector and handlers + GlobalLogCollector.setupGlobalLogCollector(new LogCollectorBuilder() + .withNamespacedResources("sa", "deployment", "configmaps", "secret") + .withClusterWideResources("nodes") + .withKubeClient(KubeResourceManager.getKubeClient()) + .withKubeCmdClient(KubeResourceManager.getKubeCmdClient()) + .withRootFolderPath(LOG_DIR.toString()) + .build()); + GlobalLogCollector.addLogCallback(() -> { + GlobalLogCollector.getGlobalLogCollector().collectFromNamespaces("default"); + GlobalLogCollector.getGlobalLogCollector().collectClusterWideResources(); + }); } protected String nsName1 = "test"; diff --git a/test-frame-test-examples/src/test/java/io/skodjob/testframe/test/integration/GlobalLogCollectorIT.java b/test-frame-test-examples/src/test/java/io/skodjob/testframe/test/integration/GlobalLogCollectorIT.java new file mode 100644 index 0000000..a26be4d --- /dev/null +++ b/test-frame-test-examples/src/test/java/io/skodjob/testframe/test/integration/GlobalLogCollectorIT.java @@ -0,0 +1,27 @@ +/* + * Copyright Skodjob authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +package io.skodjob.testframe.test.integration; + +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.fail; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class GlobalLogCollectorIT extends AbstractIT { + @Test + void testGlobalLogCollector() { + fail("Expected issue"); + } + + @AfterEach + void clean() throws IOException { + FileUtils.deleteDirectory(LOG_DIR.toFile()); + } +} diff --git a/test-frame-test-examples/src/test/java/io/skodjob/testframe/test/integration/helpers/GlobalLogCollectorTestHandler.java b/test-frame-test-examples/src/test/java/io/skodjob/testframe/test/integration/helpers/GlobalLogCollectorTestHandler.java new file mode 100644 index 0000000..f71e396 --- /dev/null +++ b/test-frame-test-examples/src/test/java/io/skodjob/testframe/test/integration/helpers/GlobalLogCollectorTestHandler.java @@ -0,0 +1,36 @@ +/* + * Copyright Skodjob authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +package io.skodjob.testframe.test.integration.helpers; + +import io.skodjob.testframe.test.integration.AbstractIT; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Override test failure for expected test issue in GlobalLogCollectorIT test + */ +public class GlobalLogCollectorTestHandler implements TestExecutionExceptionHandler { + + /** + * Check cause message and if it is from GlobalLogCollectorIT check if logs + * are collected and do not mark test as failure + * + * @param context extension context + * @param cause throwable object + * @throws Throwable throwable object + */ + @Override + public void handleTestExecutionException(ExtensionContext context, Throwable cause) throws Throwable { + if (cause.getMessage().contains("Expected issue")) { + assertTrue(AbstractIT.LOG_DIR.toFile().exists()); + assertTrue(AbstractIT.LOG_DIR.resolve("default").toFile().exists()); + assertTrue(AbstractIT.LOG_DIR.resolve("cluster-wide-resources").toFile().exists()); + } else { + throw cause; + } + } +}