/**
* Copyright 2015-2016 The OpenZipkin Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package zipkin.execjar;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javax.net.ServerSocketFactory;
import org.junit.AssumptionViolatedException;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.springframework.boot.loader.JarLauncher;
/**
* This is a JUnit Rule that allows you to test your Spring Boot exec jar.
*
* <p>It will start on a random port, and waits until the server is started before your tests
* execute.
*
* <p>Often, the test classpath interferes with your ability to test your autoconfiguration, or
* environment mappings. This class forks a process and watches it. On failure, you can look at its
* {@link #consoleOutput() console output} for details.
*/
public final class ExecJarRule implements TestRule {
public ExecJarRule() {
this.execJar = JarLauncher.class.getProtectionDomain().getCodeSource().getLocation().getFile();
}
/** Adds a variable to the environment used by the forked boot app. */
public ExecJarRule putEnvironment(String key, String value) {
environment.put(key, value);
return this;
}
/** Returns stderr and stdout dumped into the same place */
public String consoleOutput() {
return String.join("\n", console);
}
/** Lazy-chooses a server port, or returns the port the server started with */
public synchronized int port() throws IOException {
if (port != null) return port;
try (ServerSocket socket = ServerSocketFactory.getDefault().createServerSocket(0)) {
return (this.port = socket.getLocalPort());
}
}
private final String execJar;
private Map<String, String> environment = new LinkedHashMap<>();
private Integer port;
private Process bootApp;
private ConcurrentLinkedQueue<String> console = new ConcurrentLinkedQueue<>();
@Override public Statement apply(Statement base, Description description) {
return new Statement() {
public void evaluate() throws Throwable {
try {
ProcessBuilder bootBuilder = new ProcessBuilder("java", "-jar", execJar);
bootBuilder.environment().put("SERVER_PORT", String.valueOf(port()));
bootBuilder.environment().putAll(environment);
bootBuilder.redirectErrorStream(true);
bootApp = bootBuilder.start();
CountDownLatch startedOrCrashed = new CountDownLatch(1);
Thread consoleReader = new Thread(() -> {
boolean foundStartMessage = false;
try (BufferedReader reader =
new BufferedReader(new InputStreamReader(bootApp.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.indexOf("JVM running for") != -1) {
foundStartMessage = true;
startedOrCrashed.countDown();
}
console.add(line);
}
} catch (Exception e) {
} finally {
if (!foundStartMessage) startedOrCrashed.countDown();
}
});
consoleReader.setDaemon(true);
consoleReader.start();
if (!startedOrCrashed.await(10, TimeUnit.SECONDS)) {
throw new AssumptionViolatedException("Took too long to start or crash");
}
base.evaluate();
} finally {
bootApp.destroy();
}
}
};
}
}