diff --git a/src/main/java/org/kiwiproject/dropwizard/util/health/ProcessLaunchHealthCheck.java b/src/main/java/org/kiwiproject/dropwizard/util/health/ProcessLaunchHealthCheck.java new file mode 100644 index 00000000..96f3b94a --- /dev/null +++ b/src/main/java/org/kiwiproject/dropwizard/util/health/ProcessLaunchHealthCheck.java @@ -0,0 +1,48 @@ +package org.kiwiproject.dropwizard.util.health; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.kiwiproject.metrics.health.HealthCheckResults.newHealthyResult; +import static org.kiwiproject.metrics.health.HealthCheckResults.newUnhealthyResult; + +import com.codahale.metrics.health.HealthCheck; +import lombok.extern.slf4j.Slf4j; +import org.kiwiproject.base.process.ProcessHelper; +import org.kiwiproject.io.KiwiIO; + +/** + * Health check that checks if we are able to launch processes. This check attempts to launch a simple echo process + * and verifies the output is as expected. + * + * @implNote Only works on *nix-like systems, or maybe in Windows if there is an echo command installed and on the PATH. + */ +@Slf4j +public class ProcessLaunchHealthCheck extends HealthCheck { + + static final String ECHO_MESSAGE = "Launch process health check"; + + private final ProcessHelper processes; + + public ProcessLaunchHealthCheck(ProcessHelper processes) { + this.processes = processes; + } + + @Override + protected Result check() { + try { + var process = processes.launch("echo", ECHO_MESSAGE); + var line = KiwiIO.readInputStreamOf(process, UTF_8); + return resultBasedOnEchoOutput(line); + } catch (Exception e) { + LOG.trace("Process launch health check is not healthy", e); + return newUnhealthyResult("Failed launching an 'echo' process: " + e.getMessage()); + } + } + + private static Result resultBasedOnEchoOutput(String line) { + if (ECHO_MESSAGE.equals(line.stripTrailing())) { + return newHealthyResult(); + } + + return newUnhealthyResult("Output [%s] from 'echo' does not match expected [%s]", line, ECHO_MESSAGE); + } +} diff --git a/src/test/java/org/kiwiproject/dropwizard/util/health/ProcessLaunchHealthCheckTest.java b/src/test/java/org/kiwiproject/dropwizard/util/health/ProcessLaunchHealthCheckTest.java new file mode 100644 index 00000000..46b576df --- /dev/null +++ b/src/test/java/org/kiwiproject/dropwizard/util/health/ProcessLaunchHealthCheckTest.java @@ -0,0 +1,100 @@ +package org.kiwiproject.dropwizard.util.health; + +import static org.kiwiproject.base.KiwiStrings.f; +import static org.kiwiproject.dropwizard.util.health.ProcessLaunchHealthCheck.ECHO_MESSAGE; +import static org.kiwiproject.test.assertj.dropwizard.metrics.HealthCheckResultAssertions.assertThatHealthCheck; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.common.base.Charsets; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.kiwiproject.base.process.ProcessHelper; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; + +@DisplayName("ProcessLaunchHealthCheck") +class ProcessLaunchHealthCheckTest { + + private ProcessLaunchHealthCheck healthCheck; + private ProcessHelper processes; + private Process process; + + @BeforeEach + void setup() { + process = mock(Process.class); + processes = mock(ProcessHelper.class); + healthCheck = new ProcessLaunchHealthCheck(processes); + } + + @Nested + class IsHealthy { + + @Test + void whenEchoProcessCanBeLaunched() { + when(processes.launch("echo", ECHO_MESSAGE)).thenReturn(process); + + var inputStream = toInputStream(ECHO_MESSAGE); + when(process.getInputStream()).thenReturn(inputStream); + + assertThatHealthCheck(healthCheck) + .isHealthy(); + } + + /** + * Only running on Linux and MacOS since we know for a fact that 'echo' exists there. + */ + @Test + @EnabledOnOs({OS.LINUX, OS.MAC}) + void whenEchoProcessCanBeLaunched_UsingActualProcess() { + healthCheck = new ProcessLaunchHealthCheck(new ProcessHelper()); + + assertThatHealthCheck(healthCheck) + .isHealthy(); + } + } + + @Nested + class IsUnhealthy { + + @Test + void whenEchoMessageDoesNotMatchExpectedOutput() { + when(processes.launch("echo", ECHO_MESSAGE)).thenReturn(process); + + var droidsMessage = "These are not the droids you're looking for.. "; + var inputStream = toInputStream(droidsMessage); + when(process.getInputStream()).thenReturn(inputStream); + + assertThatHealthCheck(healthCheck) + .isUnhealthy() + .hasMessage(f("Output [{}] from 'echo' does not match expected [{}]", + droidsMessage, ECHO_MESSAGE)); + } + + @Test + void whenExceptionIsThrown() { + var message = "Process launch failure"; + IOException ioException = new IOException(message); + UncheckedIOException uncheckedIOException = new UncheckedIOException(ioException); + + when(processes.launch("echo", ECHO_MESSAGE)).thenThrow(uncheckedIOException); + + assertThatHealthCheck(healthCheck) + .isUnhealthy() + .hasMessageStartingWith("Failed launching an 'echo' process: ") + .hasMessageEndingWith(uncheckedIOException.getMessage()); + } + } + + private InputStream toInputStream(String value) { + return new ByteArrayInputStream(value.getBytes(Charsets.UTF_8)); + } +} +