package io.bootique.test.junit;
import io.bootique.BQRuntime;
import io.bootique.command.CommandOutcome;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.rules.ExternalResource;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
/**
* Manages a "daemon" Bootique stack within a lifecycle of the a JUnit test. This allows to start background servers so
* that tests can execute requests against them, etc. Instances should be annotated within the unit tests with
* {@link Rule} or {@link ClassRule}. E.g.:
* <pre>
* public class MyTest {
*
* @Rule
* public BQDaemonTestFactory testFactory = new BQDaemonTestFactory();
* }
* </pre>
*
* @since 0.15
*/
public class BQDaemonTestFactory extends ExternalResource {
static BQRuntimeDaemon getDaemon(Map<BQRuntime, BQRuntimeDaemon> runtimes, BQRuntime runtime) {
return Objects
.requireNonNull(runtimes.get(runtime), "Runtime is not registered with the factory: " + runtime);
}
protected Map<BQRuntime, BQRuntimeDaemon> runtimes;
@Override
protected void after() {
Map<BQRuntime, BQRuntimeDaemon> localRuntimes = this.runtimes;
if (localRuntimes != null) {
localRuntimes.values().forEach(runtime -> {
try {
runtime.stop();
} catch (Exception e) {
// ignore...
}
});
}
}
@Override
protected void before() {
this.runtimes = new HashMap<>();
}
/**
* @param <T> a covariant builder type.
* @param args a String vararg emulating shell arguments passed to a real app.
* @return a new instance of builder for the test runtime stack.
* @since 0.20
*/
public <T extends Builder<T>> Builder<T> app(String... args) {
return new Builder(runtimes, args);
}
/**
* @param runtime a runtime executing in the background.
* @return an optional object wrapping the state of the runtime execution. If present, then the runtime
* execution has finished.
* @since 0.22
*/
public Optional<CommandOutcome> getOutcome(BQRuntime runtime) {
return getDaemon(runtime).getOutcome();
}
/**
* Starts the specified runtime on the background. If startup check was specified when building the runtime with
* {@link Builder#startupCheck(Function)} and similar, blocks the calling thread until startup check succeeds or
* times out.
*
* @param runtime a runtime being tested. Must be the runtime produced and managed by this factory.
* @since 0.23
*/
public void start(BQRuntime runtime) {
getDaemon(runtime).start();
}
protected BQRuntimeDaemon getDaemon(BQRuntime runtime) {
return BQDaemonTestFactory.getDaemon(this.runtimes, runtime);
}
/**
* Shuts down the specified runtime running on the background as well as the thread pool supporting its execution.
*
* @param runtime a runtime being tested. Must be the runtime produced and managed by this factory.
* @since 0.23
*/
public void stop(BQRuntime runtime) {
getDaemon(runtime).stop();
}
// parameterization is needed to enable covariant return types in subclasses
public static class Builder<T extends Builder<T>> extends BQTestRuntimeBuilder<T> {
private static final Function<BQRuntime, Boolean> AFFIRMATIVE_STARTUP_CHECK = runtime -> true;
private Map<BQRuntime, BQRuntimeDaemon> runtimes;
private Function<BQRuntime, Boolean> startupCheck;
private long startupTimeout;
private TimeUnit startupTimeoutTimeUnit;
protected Builder(Map<BQRuntime, BQRuntimeDaemon> runtimes, String[] args) {
super(args);
this.startupTimeout = 5;
this.startupTimeoutTimeUnit = TimeUnit.SECONDS;
this.runtimes = runtimes;
this.startupCheck = AFFIRMATIVE_STARTUP_CHECK;
}
public T startupCheck(Function<BQRuntime, Boolean> startupCheck) {
this.startupCheck = Objects.requireNonNull(startupCheck);
return (T) this;
}
/**
* Adds a startup check that waits till the runtime finishes, within the
* startup timeout bounds.
*
* @return this builder
* @since 0.16
*/
public T startupAndWaitCheck() {
this.startupCheck = runtime -> BQDaemonTestFactory.getDaemon(runtimes, runtime).getOutcome().isPresent();
return (T) this;
}
public T startupTimeout(long timeout, TimeUnit unit) {
this.startupTimeout = timeout;
this.startupTimeoutTimeUnit = unit;
return (T) this;
}
/**
* Creates runtime without starting it. Can be started via {@link BQDaemonTestFactory#start(BQRuntime)}.
*
* @return newly created managed runtime.
* @since 0.23
*/
public BQRuntime createRuntime() {
BQRuntime runtime = bootique.createRuntime();
// wrap in BQRuntimeDaemon to handle thread pool shutdown and startup checks.
BQRuntimeDaemon testRuntime =
new BQRuntimeDaemon(runtime, startupCheck, startupTimeout, startupTimeoutTimeUnit);
runtimes.put(runtime, testRuntime);
return runtime;
}
/**
* Starts the test app in a background thread, blocking the test thread until the startup checker succeeds.
*
* @return {@link BQRuntime} instance. The caller doesn't need to shut it down. JUnit lifecycle takes care of it.
* @since 0.23
*/
public BQRuntime start() {
BQRuntime runtime = createRuntime();
getDaemon(runtimes, runtime).start();
return runtime;
}
}
}