Add integration tests for default launch script

This commit adds a suit of integration tests for the launch script. See
the accompanying README.adoc for further details.

Closes gh-4872
This commit is contained in:
Andy Wilkinson 2016-01-11 18:01:15 +00:00
parent c39a55a270
commit d1b47c8a8f
27 changed files with 753 additions and 0 deletions

View File

@ -19,9 +19,11 @@
<properties>
<main.basedir>${basedir}/..</main.basedir>
<java.version>1.8</java.version>
<jackson.version>2.1.2</jackson.version> <!-- Align with docker-java -->
</properties>
<modules>
<module>spring-boot-gradle-tests</module>
<module>spring-boot-launch-script-tests</module>
<module>spring-boot-security-tests</module>
</modules>
<profiles>

View File

@ -0,0 +1,75 @@
= Spring Boot Launch Script Tests
This module contains integration tests for the default launch script that is used
to make a jar file fully executable on Linux. The tests use Docker to verify the
functionality in a variety of Linux distributions.
== Setting up Docker
The setup that's required varies depending on your operating system.
=== Docker on OS X
Docker relies on Linux kernel features so the Docker Daemon must be run inside a Linux VM.
Following the https://docs.docker.com/engine/installation/mac/[OS X installation
instructions] to install Docker Toolbox which uses VirtualBox to host the required VM.
=== Docker on Linux
Install Docker as appropriate for your Linux distribution. See the
https://docs.docker.com/engine/installation/[Linux installation instructions] for more
information.
Next, add your user to the `docker` group. For example:
----
$ sudo usermod -a -G docker awilkinson
----
You may need to log out and back in again for this change to take affect and for your
user to be able to connect to the daemon.
== Preparing to run the tests
Before running the tests, you must prepare your environment according to your operating
system.
=== Preparation on OS X
The tests must be run in an environment where various environment variables including
`DOCKER_HOST` and `DOCKER_CERT_PATH` have been set:
----
$ eval $(docker-machine env default)
----
=== Preparation on Linux
Docker Daemon's default configuration on Linux uses a Unix socket for communication.
However, Docker's Java client uses HTTP by default. Docker Java's client can be configured
to use the Unix socket via the `DOCKER_URL` environment variable:
----
$ export DOCKER_URL=unix:///var/run/docker.sock
----
== Running the tests
You're now ready to run the tests. Assuming that you're in the same directory as this
README, the tests can be launched as follows:
----
$ mvn -Pdocker clean verify
----
The first time the tests are run, Docker will create the container images that are used to
run the tests. This can take several minutes, particularly if you have a slow network
connection. Subsequent runs will be faster as the images are cached locally. You can run
`docker images` to see a list of the cached images.
== Cleaning up
If you want to reclaim the disk space used by the cached images (at the expense of having
to wait for them to be downloaded and rebuilt the next time you run the tests), you can
use `docker images` to list the images and `docker rmi <image>` to delete them. See
`docker rmi --help` for further details.

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-integration-tests</artifactId>
<version>1.3.2.BUILD-SNAPSHOT</version>
</parent>
<artifactId>spring-boot-launch-script-tests</artifactId>
<packaging>jar</packaging>
<name>Spring Boot Launch Script Integration Tests</name>
<description>Spring Boot Launch Script Integration Tests</description>
<url>http://projects.spring.io/spring-boot/</url>
<organization>
<name>Pivotal Software, Inc.</name>
<url>http://www.spring.io</url>
</organization>
<properties>
<main.basedir>${basedir}/../..</main.basedir>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java</artifactId>
<version>2.1.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<profiles>
<profile>
<id>docker</id>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@ -0,0 +1,29 @@
/*
* Copyright 2012-2016 the original author or 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 org.springframework.boot.launchscript;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class LaunchScriptTestApplication {
public static void main(String[] args) {
SpringApplication.run(LaunchScriptTestApplication.class, args);
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright 2012-2016 the original author or 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 org.springframework.boot.launchscript;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class LaunchVerficationController {
@RequestMapping("/")
public String verifyLaunch() {
return "Launched";
}
}

View File

@ -0,0 +1,365 @@
/*
* Copyright 2012-2016 the original author or 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 org.springframework.boot.launchscript;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.DockerCmd;
import com.github.dockerjava.api.model.Frame;
import com.github.dockerjava.core.CompressArchiveUtil;
import com.github.dockerjava.core.DockerClientBuilder;
import com.github.dockerjava.core.DockerClientConfig;
import com.github.dockerjava.core.command.AttachContainerResultCallback;
import com.github.dockerjava.core.command.BuildImageResultCallback;
import com.github.dockerjava.jaxrs.AbstrSyncDockerCmdExec;
import com.github.dockerjava.jaxrs.DockerCmdExecFactoryImpl;
import org.hamcrest.Matcher;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.springframework.boot.ansi.AnsiColor;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertThat;
/**
* Integration tests for Spring Boot's launch script on OSs that use SysVinit.
*
* @author Andy Wilkinson
*/
@RunWith(Parameterized.class)
public class SysVinitLaunchScriptIT {
private final SpringBootDockerCmdExecFactory commandExecFactory = new SpringBootDockerCmdExecFactory();
private static final char ESC = 27;
private final String os;
private final String version;
@Parameters(name = "{0} {1}")
public static List<Object[]> parameters() {
List<Object[]> parameters = new ArrayList<Object[]>();
for (File os : new File("src/test/resources/conf").listFiles()) {
for (File version : os.listFiles()) {
parameters.add(new Object[] { os.getName(), version.getName() });
}
}
return parameters;
}
public SysVinitLaunchScriptIT(String os, String version) {
this.os = os;
this.version = version;
}
@Test
public void statusWhenStopped() throws Exception {
String output = doTest("status-when-stopped.sh");
assertThat(output, containsString("Status: 3"));
assertThat(output, containsColoredString(AnsiColor.RED, "Not running"));
}
@Test
public void statusWhenStarted() throws Exception {
String output = doTest("status-when-started.sh");
assertThat(output, containsString("Status: 0"));
assertThat(output, containsColoredString(AnsiColor.GREEN,
"Started [" + extractPid(output) + "]"));
}
@Test
public void statusWhenKilled() throws Exception {
String output = doTest("status-when-killed.sh");
assertThat(output, containsString("Status: 1"));
assertThat(output, containsColoredString(AnsiColor.RED,
"Not running (process " + extractPid(output) + " not found)"));
}
@Test
public void stopWhenStopped() throws Exception {
String output = doTest("stop-when-stopped.sh");
assertThat(output, containsString("Status: 0"));
assertThat(output, containsColoredString(AnsiColor.YELLOW,
"Not running (pidfile not found)"));
}
@Test
public void startWhenStarted() throws Exception {
String output = doTest("start-when-started.sh");
assertThat(output, containsString("Status: 0"));
assertThat(output, containsColoredString(AnsiColor.YELLOW,
"Already running [" + extractPid(output) + "]"));
}
@Test
public void restartWhenStopped() throws Exception {
String output = doTest("restart-when-stopped.sh");
assertThat(output, containsString("Status: 0"));
assertThat(output, containsColoredString(AnsiColor.YELLOW,
"Not running (pidfile not found)"));
assertThat(output, containsColoredString(AnsiColor.GREEN,
"Started [" + extractPid(output) + "]"));
}
@Test
public void restartWhenStarted() throws Exception {
String output = doTest("restart-when-started.sh");
assertThat(output, containsString("Status: 0"));
assertThat(output, containsColoredString(AnsiColor.GREEN,
"Started [" + extract("PID1", output) + "]"));
assertThat(output, containsColoredString(AnsiColor.GREEN,
"Stopped [" + extract("PID1", output) + "]"));
assertThat(output, containsColoredString(AnsiColor.GREEN,
"Started [" + extract("PID2", output) + "]"));
}
@Test
public void startWhenStopped() throws Exception {
String output = doTest("start-when-stopped.sh");
assertThat(output, containsString("Status: 0"));
assertThat(output, containsColoredString(AnsiColor.GREEN,
"Started [" + extractPid(output) + "]"));
}
@Test
public void basicLaunch() throws Exception {
doLaunch("basic-launch.sh");
}
@Test
public void launchWithSingleCommandLineArgument() throws Exception {
doLaunch("launch-with-single-command-line-argument.sh");
}
@Test
public void launchWithMultipleCommandLineArguments() throws Exception {
doLaunch("launch-with-multiple-command-line-arguments.sh");
}
@Test
public void launchWithSingleRunArg() throws Exception {
doLaunch("launch-with-single-run-arg.sh");
}
@Test
public void launchWithMultipleRunArgs() throws Exception {
doLaunch("launch-with-multiple-run-args.sh");
}
@Test
public void launchWithSingleJavaOpt() throws Exception {
doLaunch("launch-with-single-java-opt.sh");
}
@Test
public void launchWithMultipleJavaOpts() throws Exception {
doLaunch("launch-with-multiple-java-opts.sh");
}
private void doLaunch(String script) throws Exception {
assertThat(doTest(script), containsString("Launched"));
}
private String doTest(String script) throws Exception {
DockerClient docker = createClient();
String imageId = buildImage(docker);
String container = createContainer(docker, imageId, script);
copyFilesToContainer(docker, container, script);
docker.startContainerCmd(container).exec();
StringBuilder output = new StringBuilder();
AttachContainerResultCallback resultCallback = docker
.attachContainerCmd(container).withStdOut(true).withStdErr(true)
.withFollowStream(true).withLogs(true)
.exec(new AttachContainerResultCallback() {
@Override
public void onNext(Frame item) {
output.append(new String(item.getPayload()));
super.onNext(item);
}
});
resultCallback.awaitCompletion(60, TimeUnit.SECONDS).close();
docker.waitContainerCmd(container).exec();
return output.toString();
}
private DockerClient createClient() {
DockerClientConfig config = DockerClientConfig.createDefaultConfigBuilder()
.build();
DockerClient docker = DockerClientBuilder.getInstance(config)
.withDockerCmdExecFactory(this.commandExecFactory).build();
return docker;
}
private String buildImage(DockerClient docker) {
BuildImageResultCallback resultCallback = new BuildImageResultCallback();
String dockerfile = "src/test/resources/conf/" + this.os + "/" + this.version
+ "/Dockerfile";
docker.buildImageCmd(new File(dockerfile)).exec(resultCallback);
String imageId = resultCallback.awaitImageId();
return imageId;
}
private String createContainer(DockerClient docker, String imageId,
String testScript) {
return docker.createContainerCmd(imageId).withTty(false).withCmd("/bin/bash",
"-c", "chmod +x " + testScript + " && ./" + testScript).exec().getId();
}
private void copyFilesToContainer(DockerClient docker, final String container,
String script) {
copyToContainer(docker, container, findApplication());
copyToContainer(docker, container,
new File("src/test/resources/scripts/test-functions.sh"));
copyToContainer(docker, container,
new File("src/test/resources/scripts/" + script));
}
private void copyToContainer(DockerClient docker, final String container,
final File file) {
this.commandExecFactory.createCopyToContainerCmdExec()
.exec(new CopyToContainerCmd(container, file));
}
private File findApplication() {
File targetDir = new File("target");
for (File file : targetDir.listFiles()) {
if (file.getName().startsWith("spring-boot-launch-script-tests")
&& file.getName().endsWith(".jar")
&& !file.getName().endsWith("-sources.jar")) {
return file;
}
}
throw new IllegalStateException(
"Could not find test application in target directory. Have you built it (mvn package)?");
}
private Matcher<String> containsColoredString(AnsiColor color, String string) {
return containsString(ESC + "[0;" + color + "m" + string + ESC + "[0m");
}
private String extractPid(String output) {
return extract("PID", output);
}
private String extract(String label, String output) {
Pattern pattern = Pattern.compile(".*" + label + ": ([0-9]+).*", Pattern.DOTALL);
java.util.regex.Matcher matcher = pattern.matcher(output);
if (matcher.matches()) {
return matcher.group(1);
}
throw new IllegalArgumentException(
"Failed to extract " + label + " from output: " + output);
}
private static final class CopyToContainerCmdExec
extends AbstrSyncDockerCmdExec<CopyToContainerCmd, Void> {
private CopyToContainerCmdExec(WebTarget baseResource,
DockerClientConfig dockerClientConfig) {
super(baseResource, dockerClientConfig);
}
@Override
protected Void execute(CopyToContainerCmd command) {
try {
InputStream streamToUpload = new FileInputStream(CompressArchiveUtil
.archiveTARFiles(command.getFile().getParentFile(),
Arrays.asList(command.getFile()),
command.getFile().getName()));
WebTarget webResource = getBaseResource().path("/containers/{id}/archive")
.resolveTemplate("id", command.getContainer());
webResource.queryParam("path", ".")
.queryParam("noOverwriteDirNonDir", false).request()
.put(Entity.entity(streamToUpload, "application/x-tar")).close();
return null;
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
private static final class CopyToContainerCmd implements DockerCmd<Void> {
private final String container;
private final File file;
private CopyToContainerCmd(String container, File file) {
this.container = container;
this.file = file;
}
public String getContainer() {
return this.container;
}
public File getFile() {
return this.file;
}
@Override
public void close() {
}
}
private static final class SpringBootDockerCmdExecFactory
extends DockerCmdExecFactoryImpl {
private SpringBootDockerCmdExecFactory() {
withClientRequestFilters(new ClientRequestFilter() {
@Override
public void filter(ClientRequestContext requestContext)
throws IOException {
// Workaround for https://go-review.googlesource.com/#/c/3821/
requestContext.getHeaders().add("Connection", "close");
}
});
}
private CopyToContainerCmdExec createCopyToContainerCmdExec() {
return new CopyToContainerCmdExec(getBaseResource(), getDockerClientConfig());
}
}
}

View File

@ -0,0 +1,11 @@
FROM centos:5.11
RUN yum install -y wget && \
yum install -y system-config-services && \
yum install -y curl && \
wget --no-cookies \
--no-check-certificate \
--header "Cookie: gpw_e24=http%3A%2F%2Fwww.oracle.com%2F; oraclelicense=accept-securebackup-cookie" \
--output-document jdk.rpm \
http://download.oracle.com/otn-pub/java/jdk/8u66-b17/jdk-8u66-linux-x64.rpm && \
yum --nogpg localinstall -y jdk.rpm && \
rm -f jdk.rpm

View File

@ -0,0 +1,11 @@
FROM centos:6.7
RUN yum install -y wget && \
yum install -y system-config-services && \
yum install -y curl && \
wget --no-cookies \
--no-check-certificate \
--header "Cookie: gpw_e24=http%3A%2F%2Fwww.oracle.com%2F; oraclelicense=accept-securebackup-cookie" \
--output-document jdk.rpm \
http://download.oracle.com/otn-pub/java/jdk/8u66-b17/jdk-8u66-linux-x64.rpm && \
yum --nogpg localinstall -y jdk.rpm && \
rm -f jdk.rpm

View File

@ -0,0 +1,8 @@
FROM ubuntu:14.04.3
RUN apt-get install -y software-properties-common && \
add-apt-repository ppa:webupd8team/java -y && \
apt-get update && \
echo oracle-java7-installer shared/accepted-oracle-license-v1-1 select true | /usr/bin/debconf-set-selections && \
apt-get install -y oracle-java8-installer && \
apt-get install -y curl && \
apt-get clean

View File

@ -0,0 +1,13 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>

View File

@ -0,0 +1,5 @@
source ./test-functions.sh
install_service
start_service
await_app
curl -s http://127.0.0.1:8080/

View File

@ -0,0 +1,5 @@
source ./test-functions.sh
install_service
start_service --server.port=8081 --server.context-path=/test
await_app http://127.0.0.1:8081/test/
curl -s http://127.0.0.1:8081/test/

View File

@ -0,0 +1,6 @@
source ./test-functions.sh
echo 'JAVA_OPTS="-Dserver.port=8081 -Dserver.context-path=/test"' > /spring-boot-app.conf
install_service
start_service
await_app http://127.0.0.1:8081/test/
curl -s http://127.0.0.1:8081/test/

View File

@ -0,0 +1,6 @@
source ./test-functions.sh
echo 'RUN_ARGS="--server.port=8081 --server.context-path=/test"' > /spring-boot-app.conf
install_service
start_service
await_app http://127.0.0.1:8081/test/
curl -s http://127.0.0.1:8081/test/

View File

@ -0,0 +1,5 @@
source ./test-functions.sh
install_service
start_service --server.port=8081
await_app http://127.0.0.1:8081/
curl -s http://127.0.0.1:8081/

View File

@ -0,0 +1,6 @@
source ./test-functions.sh
echo 'JAVA_OPTS=-Dserver.port=8081' > /spring-boot-app.conf
install_service
start_service
await_app http://127.0.0.1:8081/
curl -s http://127.0.0.1:8081/

View File

@ -0,0 +1,6 @@
source ./test-functions.sh
echo 'RUN_ARGS=--server.port=8081' > /spring-boot-app.conf
install_service
start_service
await_app http://127.0.0.1:8081/
curl -s http://127.0.0.1:8081/

View File

@ -0,0 +1,7 @@
source ./test-functions.sh
install_service
start_service
echo "PID1: $(cat /var/run/spring-boot-app/spring-boot-app.pid)"
restart_service
echo "Status: $?"
echo "PID2: $(cat /var/run/spring-boot-app/spring-boot-app.pid)"

View File

@ -0,0 +1,5 @@
source ./test-functions.sh
install_service
restart_service
echo "Status: $?"
echo "PID: $(cat /var/run/spring-boot-app/spring-boot-app.pid)"

View File

@ -0,0 +1,6 @@
source ./test-functions.sh
install_service
start_service
echo "PID: $(cat /var/run/spring-boot-app/spring-boot-app.pid)"
start_service
echo "Status: $?"

View File

@ -0,0 +1,5 @@
source ./test-functions.sh
install_service
start_service
echo "Status: $?"
echo "PID: $(cat /var/run/spring-boot-app/spring-boot-app.pid)"

View File

@ -0,0 +1,8 @@
source ./test-functions.sh
install_service
start_service
pid=$(cat /var/run/spring-boot-app/spring-boot-app.pid)
echo "PID: $pid"
kill -9 $pid
status_service
echo "Status: $?"

View File

@ -0,0 +1,6 @@
source ./test-functions.sh
install_service
start_service
status_service
echo "Status: $?"
echo "PID: $(cat /var/run/spring-boot-app/spring-boot-app.pid)"

View File

@ -0,0 +1,4 @@
source ./test-functions.sh
install_service
status_service
echo "Status: $?"

View File

@ -0,0 +1,4 @@
source ./test-functions.sh
install_service
stop_service
echo "Status: $?"

View File

@ -0,0 +1,40 @@
install_service() {
mv /spring-boot-launch-script-tests-*.jar /spring-boot-app.jar
chmod +x /spring-boot-app.jar
ln -s /spring-boot-app.jar /etc/init.d/spring-boot-app
}
start_service() {
service spring-boot-app start $@
}
restart_service() {
service spring-boot-app restart
}
status_service() {
service spring-boot-app status
}
stop_service() {
service spring-boot-app stop
}
await_app() {
if [ -z $1 ]
then
url=http://127.0.0.1:8080
else
url=$1
fi
end=$(date +%s)
let "end+=30"
until curl -s $url > /dev/null
do
now=$(date +%s)
if [[ $now -ge $end ]]; then
break
fi
sleep 1
done
}

View File

@ -5,6 +5,7 @@
<suppressions>
<suppress files="SpringApplicationTests\.java" checks="FinalClass" />
<suppress files=".+Configuration\.java" checks="HideUtilityClassConstructor" />
<suppress files="LaunchScriptTestApplication\.java" checks="HideUtilityClassConstructor" />
<suppress files="SignalUtils\.java" checks="IllegalImport" />
<suppress files="[\\/]src[\\/]test[\\/]java[\\/]cli[\\/]command[\\/]" checks="ImportControl" />
<suppress files="[\\/]src[\\/]main[\\/]java[\\/]sample[\\/]" checks="ImportControl" />