+ * Gasper provides a simple to use JUnit {@link TestRule} that can be used to build integration tests with simple apps, like REST micro-services. You can configure Gasper easily with a builder interface. Gasper will start the application before test class and stop it after tests completes.
+ *
+ * Before running {@link GasperBuilder#build()} method, you can reconfigure those default configurations to your needs.
+ *
+ *
) () ->
+ CharStreams.toString(new InputStreamReader(is, Charsets.UTF_8)), "20160305:201329");
+ }
+
+ /**
+ * Creates a builder interface {@link GasperBuilder} that can be used to configure Gasper.
+ *
+ * You can also use, already created, configurations by using method {@link #configurations()} for
+ * convenience.
+ * @return a configure interface for configuration purposes
+ */
+ public static GasperBuilder configure() {
+ return new GasperBuilder();
+ }
+
+ /**
+ * Retrieves {@link GasperConfigurations} which hold some pre configured {@link GasperBuilder} instances
+ * and can be used for convenience.
+ * @return a pre configured configurations
+ */
+ public static GasperConfigurations configurations() {
+ return new GasperConfigurations();
+ }
+
+ /**
+ * Use this method to get port on which Gasper runs your test application.
+ * @return a usually random port on which Gasper runs your application
+ */
+ public Integer getPort() {
+ return settings.getPort();
+ }
+
+ /**
+ * Use this method to get full address to your test application that Gasper runs. It usually
+ * contains a random port.
+ * @return a full address to running application
+ */
+ public String getAddress() {
+ return settings.getEndpoint().fullAddress();
+ }
+
+ @Override
+ public Statement apply(Statement base, Description description) {
+ return tryToExecute((EidPreconditions.UnsafeSupplier) () -> {
+ setup();
+ before();
+ return new GasperStatement(base, this::after);
+ }, "20160305:004035");
+ }
+
+ protected interface RunnerCreator {
+ default Gasper create(Settings settings) {
+ return new Gasper(settings);
+ }
+ }
+
+ private void setup() {
+ log(FIGLET);
+ MavenResolver resolver = new MavenResolver(settings.getPomfile());
+ artifact = resolver.getBuildArtifact(settings.getPackaging(), settings.getClassifier());
+ File workingDirectory = resolver.getBuildDirectory();
+ List command = buildCommand();
+ log("Command to be executed: \"%s\"", command.stream().collect(Collectors.joining(" ")));
+ executor = new Executor(command, workingDirectory, settings);
+ }
+
+ private void before() throws IOException {
+ executor.start();
+ log("All looks ready, running tests...");
+ }
+
+ private void after() {
+ log("Testing on server completed.");
+ executor.stop();
+ }
+
+ @RequiredArgsConstructor
+ private static class GasperStatement extends Statement {
+ private final Statement base;
+ private final Procedure procedure;
+
+ @Override
+ public void evaluate() throws Throwable {
+ try {
+ base.evaluate();
+ } finally {
+ procedure.execute();
+ }
+ }
+ }
+
+ @FunctionalInterface
+ private interface Procedure {
+ void execute();
+ }
+
+ private List buildCommand() {
+ List command = new ArrayList<>();
+ command.add("java");
+ buildJavaOptions(command);
+ command.add("-jar");
+ command.add(artifact.toAbsolutePath().toString());
+ return command;
+ }
+
+ private void buildJavaOptions(List command) {
+ command.addAll(settings.getJvmOptions());
+ command.addAll(settings.getSystemProperties().entrySet().stream()
+ .map(entry -> format("-D%s=%s", entry.getKey(), entry.getValue()))
+ .collect(Collectors.toList())
+ );
+ }
+
+ private void log(String frmt, Object... args) {
+ ensureLogger();
+ logger.info(format(frmt, args));
+ }
+
+ private void ensureLogger() {
+ if (logger == null) {
+ logger = new Logger(log, settings);
+ }
+ }
+}
diff --git a/src/main/java/pl/wavesoftware/gasper/GasperBuilder.java b/src/main/java/pl/wavesoftware/gasper/GasperBuilder.java
new file mode 100644
index 0000000..76534fa
--- /dev/null
+++ b/src/main/java/pl/wavesoftware/gasper/GasperBuilder.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright (c) 2016 Wave Software
+ *
+ * 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 pl.wavesoftware.gasper;
+
+import org.slf4j.event.Level;
+import pl.wavesoftware.eid.utils.EidPreconditions;
+import pl.wavesoftware.gasper.internal.Executor;
+import pl.wavesoftware.gasper.internal.HttpEndpoint;
+import pl.wavesoftware.gasper.internal.Settings;
+import pl.wavesoftware.gasper.internal.maven.MavenResolver;
+
+import java.net.ServerSocket;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import static pl.wavesoftware.eid.utils.EidPreconditions.tryToExecute;
+
+/**
+ * This is builder interface for {@link Gasper}. You can use it to configure it to your needs.
+ *
+ * Methods implements fluent interface for ease of use.
+ *
+ *
Example
+ *
+ * private final int port = 11909;
+ * private final String webContext = "/test";
+ * private final String systemPropertyForPort = "swarm.http.port";
+ *
+ * @ClassRule
+ * public static Gasper gasper = Gasper.configure()
+ * .silentGasperMessages()
+ * .usingSystemPropertyForPort(systemPropertyForPort)
+ * .withSystemProperty("swarm.context.path", webContext)
+ * .withSystemProperty(systemPropertyForPort, String.valueOf(port))
+ * .withJVMOptions("-server", "-Xms1G", "-Xmx1G", "-XX:+UseConcMarkSweepGC")
+ * .withMaxStartupTime(100)
+ * .withMaxDeploymentTime(20)
+ * .withEnvironmentVariable("jdbc.password", "S3CreT!1")
+ * .withTestApplicationLoggingOnConsole()
+ * .usingPomFile(Paths.get("pom.xml"))
+ * .withArtifactPackaging("jar")
+ * .waitForWebContext(webContext)
+ * .withArtifactClassifier("swarm")
+ * .usingWebContextChecker(GasperBuilderTest::checkContext)
+ * .withPort(port)
+ * .build();
+ *
+ *
+ * @author Krzysztof Suszyński
+ * @since 2016-03-05
+ */
+public final class GasperBuilder implements Gasper.RunnerCreator {
+
+ private String packaging = MavenResolver.DEFAULT_PACKAGING;
+ private String classifier = MavenResolver.DEFAULT_CLASSIFIER;
+ private Map systemProperties = new LinkedHashMap<>();
+ private List jvmOptions = new ArrayList<>();
+ private Map environment = new LinkedHashMap<>();
+ private String systemPropertyForPort;
+ private Integer port;
+ private boolean inheritIO = false;
+ private String context = Gasper.DEFAULT_CONTEXT;
+ private int portAvailableMaxTime = Gasper.DEFAULT_PORT_AVAILABLE_MAX_SECONDS;
+ private int deploymentMaxTime = Gasper.DEFAULT_DEPLOYMENT_MAX_SECONDS;
+ private Function contextChecker = Executor.DEFAULT_CONTEXT_CHECKER;
+ private Path pomfile = Paths.get(MavenResolver.DEFAULT_POM);
+ private Level level = Level.INFO;
+
+ protected GasperBuilder() {}
+
+ /**
+ * Change the artifact packaging. By default it is read from your pom.xml
file. Use it to point
+ * to other artifact.
+ *
+ * @param packaging a Java packaging, can be something like jar
or war
.
+ * @return fluent interface returning self for chaining
+ */
+ public GasperBuilder withArtifactPackaging(String packaging) {
+ this.packaging = packaging;
+ return this;
+ }
+
+ /**
+ * Change the artifact classifier. By default it is read from your pom.xml
file. Use it to point
+ * to other artifact.
+ *
+ * @param classifier a Maven classifier, can be something like shade
or swarm
.
+ * @return fluent interface returning self for chaining
+ */
+ public GasperBuilder withArtifactClassifier(String classifier) {
+ this.classifier = classifier;
+ return this;
+ }
+
+ /**
+ * Sets a environment variable to be set for your test application
+ *
+ * @param key an environment key
+ * @param value an environment value
+ * @return fluent interface returning self for chaining
+ */
+ public GasperBuilder withEnvironmentVariable(String key, String value) {
+ environment.put(key, value);
+ return this;
+ }
+
+ /**
+ * Sets a Java system property (for ex.: -Dserver.ssl=true
) variable to be set for
+ * your test application.
+ *
+ * @param key a system property key without -D
sign
+ * @param value a system property value
+ * @return fluent interface returning self for chaining
+ */
+ public GasperBuilder withSystemProperty(String key, String value) {
+ systemProperties.put(key, value);
+ return this;
+ }
+
+ /**
+ * Sets a JVM options (for ex.: -Xmx2G
) to be set for your test application.
+ *
+ * @param options a list of JVM options in the same form as they will be given to process
+ * @return fluent interface returning self for chaining
+ */
+ public GasperBuilder withJVMOptions(String... options) {
+ Collections.addAll(jvmOptions, options);
+ return this;
+ }
+
+ /**
+ * Sets the port to be used for starting your test application. The port must be available and user
+ * must have permission to use it. By default port is automatically calculated to be random, free
+ * one. Use this only if you don't like automatic port lookup.
+ *
+ * @param port a port to be used
+ * @return fluent interface returning self for chaining
+ */
+ public GasperBuilder withPort(int port) {
+ this.port = port;
+ return this;
+ }
+
+ /**
+ * Configures what system property use to set port in your test application. For WildFly Swarm
+ * and Sprint Boot this is already configured in {@link GasperConfigurations} methods
+ * {@link GasperConfigurations#wildflySwarm()} and {@link GasperConfigurations#springBoot()}.
+ * Use it if you must pass other system property.
+ *
+ * @param systemPropertyForPort a system property to be used to change te port on which your
+ * test application will run.
+ * @return fluent interface returning self for chaining
+ */
+ public GasperBuilder usingSystemPropertyForPort(String systemPropertyForPort) {
+ this.systemPropertyForPort = systemPropertyForPort;
+ return this;
+ }
+
+ /**
+ * Change the pom.xml
to be used. Gasper will read your project settings from it,
+ * like artifactId
, packaging
, classifier
, version
+ * and build directory
to locate artifact to be run as test application.
+ *
+ * @param pomfile a custom pom.xml
to be used to read configuration properties
+ * @return fluent interface returning self for chaining
+ */
+ public GasperBuilder usingPomFile(Path pomfile) {
+ this.pomfile = pomfile;
+ return this;
+ }
+
+ /**
+ * Configures your test application to logs it's messages on console instead of log file.
+ *
+ * @return fluent interface returning self for chaining
+ */
+ public GasperBuilder withTestApplicationLoggingOnConsole() {
+ return withTestApplicationLoggingOnConsole(true);
+ }
+
+ /**
+ * Configures your test application whether to logs it's messages on console instead
+ * of log file or not.
+ *
+ * @param inheritIO if true, the test application will logs it's messages on console,
+ * if not messages will be forwarder to [system-temp]/gasper.log
+ * @return fluent interface returning self for chaining
+ */
+ public GasperBuilder withTestApplicationLoggingOnConsole(boolean inheritIO) {
+ this.inheritIO = inheritIO;
+ return this;
+ }
+
+ /**
+ * Sets maximum wait time for your test application to open HTTP port. Tests
+ * will fail if your test application will not open requested port in that time.
+ *
+ * @param seconds maximum wait time for open port in seconds
+ * @return fluent interface returning self for chaining
+ */
+ public GasperBuilder withMaxStartupTime(int seconds) {
+ this.portAvailableMaxTime = seconds;
+ return this;
+ }
+
+ /**
+ * Sets maximum wait time for your test application to deploy expected web context.
+ * Tests will fail if your test application will not deploy web context in time.
+ *
+ * @param seconds maximum wait time for deployment in seconds
+ * @return fluent interface returning self for chaining
+ */
+ public GasperBuilder withMaxDeploymentTime(int seconds) {
+ this.deploymentMaxTime = seconds;
+ return this;
+ }
+
+ /**
+ * Sets te web context to wait for. Gasper by default will try to execute
+ * HEAD
request to that address until it became available. By
+ * default te web context id just "/"
.
+ *
+ * @param context the web context to wait for, by default "/"
+ * @return fluent interface returning self for chaining
+ */
+ public GasperBuilder waitForWebContext(String context) {
+ this.context = context;
+ return this;
+ }
+
+ /**
+ * Change web context checker that will be used to check if web context is
+ * deployed. Gasper by default will try to execute HEAD
request
+ * to that address until it became available.
+ *
+ * @param contextChecker a function to use to test if web context id up
+ * @return fluent interface returning self for chaining
+ */
+ public GasperBuilder usingWebContextChecker(Function contextChecker) {
+ this.contextChecker = contextChecker;
+ return this;
+ }
+
+ /**
+ * Silent Gasper log messages.
+ *
+ * @return fluent interface returning self for chaining
+ */
+ public GasperBuilder silentGasperMessages() {
+ usingLogLevel(Level.WARN);
+ return this;
+ }
+
+ /**
+ * Sets log level for Gasper to limit logging.
+ * @param level a SLF log level
+ * @return fluent interface returning self for chaining
+ */
+ public GasperBuilder usingLogLevel(Level level) {
+ this.level = level;
+ return this;
+ }
+
+ /**
+ * Builds final Gasper instance with all given variables
+ * @return a Gasper {@link org.junit.rules.TestRule}
+ */
+ public Gasper build() {
+ if (port == null) {
+ port = findNotBindedPort();
+ }
+ if (systemPropertyForPort != null) {
+ withSystemProperty(systemPropertyForPort, port.toString());
+ }
+ Settings settings = new Settings(
+ packaging, classifier, port,
+ systemProperties, jvmOptions, environment,
+ inheritIO, context, contextChecker,
+ portAvailableMaxTime, deploymentMaxTime,
+ pomfile, level
+ );
+ return create(settings);
+ }
+
+ private static Integer findNotBindedPort() {
+ return tryToExecute((EidPreconditions.UnsafeSupplier) () -> {
+ try (ServerSocket socket = new ServerSocket(0)) {
+ return socket.getLocalPort();
+ }
+ }, "20160305:202934");
+
+ }
+}
diff --git a/src/main/java/pl/wavesoftware/gasper/GasperConfigurations.java b/src/main/java/pl/wavesoftware/gasper/GasperConfigurations.java
new file mode 100644
index 0000000..b27c59e
--- /dev/null
+++ b/src/main/java/pl/wavesoftware/gasper/GasperConfigurations.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2016 Wave Software
+ *
+ * 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 pl.wavesoftware.gasper;
+
+/**
+ * This class holds supported and tested configurations for servers.
+ *
+ * Configuration in this class are fairly tested and can serve as a base for configuration.
+ * You shouldn't try to use this class directly. Use instead {@link Gasper#configurations()} for entry point.
+ *
+ * Example:
+ *
+ * @ClassRule
+ * public Gasper runner = Gasper.configurations()
+ * .wildflySwarm()
+ * .build();
+ *
+ *
+ * More info in {@link Gasper} javadoc
+ *
+ * @author Krzysztof Suszyński
+ * @since 2016-03-05
+ */
+public final class GasperConfigurations {
+ public static final String WILDFLY_SWARM = "swarm.http.port";
+ public static final String SPRING_BOOT = "server.port";
+ protected GasperConfigurations() {}
+
+ /**
+ * This method returns pre-configured Gasper configuration to use with WildFly Swarm.
+ *
+ * You can use it directly or use {@link GasperBuilder} interface to re-configure it to you needs.
+ *
+ * To use it in JUnit execute method {@link GasperBuilder#build()}
+ * @return pre-configured {@link GasperBuilder} to use with WildFly Swarm.
+ */
+ public GasperBuilder wildflySwarm() {
+ return Gasper.configure()
+ .withArtifactPackaging("jar")
+ .withArtifactClassifier("swarm")
+ .usingSystemPropertyForPort(GasperConfigurations.WILDFLY_SWARM);
+ }
+
+ /**
+ * This method returns pre-configured Gasper configure to use with Spring Boot.
+ *
+ * You can use it directly or use {@link GasperBuilder} interface to re-configure it to you needs.
+ *
+ * To use it in JUnit execute method {@link GasperBuilder#build()}
+ * @return pre-configured {@link GasperBuilder} to use with Spring Boot.
+ */
+ public GasperBuilder springBoot() {
+ return Gasper.configure()
+ .withArtifactPackaging("jar")
+ .usingSystemPropertyForPort(GasperConfigurations.SPRING_BOOT);
+ }
+}
diff --git a/src/main/java/pl/wavesoftware/gasper/internal/Executor.java b/src/main/java/pl/wavesoftware/gasper/internal/Executor.java
new file mode 100644
index 0000000..92c7ae4
--- /dev/null
+++ b/src/main/java/pl/wavesoftware/gasper/internal/Executor.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (c) 2016 Wave Software
+ *
+ * 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 pl.wavesoftware.gasper.internal;
+
+import com.mashape.unirest.http.HttpResponse;
+import com.mashape.unirest.http.Unirest;
+import com.mashape.unirest.http.exceptions.UnirestException;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import pl.wavesoftware.eid.exceptions.Eid;
+import pl.wavesoftware.eid.exceptions.EidIllegalStateException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.ServerSocket;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+
+import static java.lang.String.format;
+
+/**
+ * @author Krzysztof Suszyński
+ * @since 2016-03-05
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class Executor {
+ public static final int WAIT_STEP = 125;
+ public static final int WAIT_STEPS_IN_SECOND = 8;
+ public static final Function DEFAULT_CONTEXT_CHECKER = Executor::check;
+ private static final int HTTP_OK = 200;
+ private static final int HTTP_BAD_REQUEST = 400;
+ private final List command;
+ private final File workingDirectory;
+ private final Settings settings;
+ private Process process;
+ private Logger logger;
+
+ public void start() throws IOException {
+ ProcessBuilder pb = new ProcessBuilder(command);
+ pb.directory(workingDirectory);
+ if (settings.isInheritIO()) {
+ pb.inheritIO();
+ } else {
+ logToFile(pb);
+ }
+ if (!settings.getEnvironment().isEmpty()) {
+ pb.environment().putAll(settings.getEnvironment());
+ }
+ log("Starting server process");
+ process = pb.start();
+
+ startAndWaitForPort();
+ waitForHttpContext();
+ }
+
+ public void stop() {
+ log("Stopping server process");
+ process.destroy();
+ }
+
+ private void waitForHttpContext() {
+ String context = settings.getContext();
+ int maxWait = settings.getDeploymentMaxTime();
+ log("Waiting for deployment for context: \"%s\" to happen...", context);
+ boolean ok = waitForContextToBecomeAvailable(context, maxWait);
+ if (!ok) {
+ throw new EidIllegalStateException(new Eid("20160305:123206"),
+ "Context %s in not available after waiting %s seconds, aborting!",
+ context, maxWait
+ );
+ }
+ }
+
+ private boolean waitForContextToBecomeAvailable(String context, int maxSeconds) {
+ return waitOnProcess(maxSeconds, (step) -> {
+ if (isContextAvailable()) {
+ int waited = WAIT_STEP * step;
+ log("Context \"%s\" became available after ~%dms!", context, waited);
+ return true;
+ }
+ return false;
+ });
+ }
+
+ private boolean waitForPortToBecomeAvailable(int port, int maxSeconds) {
+ return waitOnProcess(maxSeconds, (step) -> {
+ if (isPortTaken(port)) {
+ int waited = WAIT_STEP * step;
+ log("Port %d became available after ~%dms!", port, waited);
+ return true;
+ }
+ return false;
+ });
+ }
+
+ private boolean waitOnProcess(int maxSeconds, Function supplier) {
+ for (int i = 1; i <= maxSeconds * WAIT_STEPS_IN_SECOND; i++) {
+ try {
+ process.waitFor(WAIT_STEP, TimeUnit.MILLISECONDS);
+ if (supplier.apply(i)) {
+ return true;
+ }
+ } catch (InterruptedException e) {
+ log.error("Tried to wait " + WAIT_STEP + "ms, failed: " + e.getLocalizedMessage(), e);
+ Thread.currentThread().interrupt();
+ }
+ }
+ return false;
+ }
+
+ private static Boolean check(HttpEndpoint endpoint) {
+ String address = endpoint.fullAddress();
+ try {
+ HttpResponse response = Unirest.head(address).asBinary();
+ int status = response.getStatus();
+ return status >= HTTP_OK && status < HTTP_BAD_REQUEST;
+ } catch (UnirestException e) {
+ EidIllegalStateException ex = new EidIllegalStateException(new Eid("20160305:125410"), e);
+ log.error(ex.getEid().makeLogMessage("Can't make http request - %s", e.getLocalizedMessage()), ex);
+ return false;
+ }
+ }
+
+ private boolean isContextAvailable() {
+ HttpEndpoint endpoint = settings.getEndpoint();
+ return settings.getContextChecker().apply(endpoint);
+ }
+
+ private void startAndWaitForPort() {
+ Integer port = settings.getPort();
+ log("Waiting for port: %d to became active...", port);
+ boolean ok = waitForPortToBecomeAvailable(port, settings.getPortAvailableMaxTime());
+ if (!ok) {
+ throw new EidIllegalStateException(new Eid("20160305:003452"),
+ "Process %s probably didn't started well after maximum wait time is reached: %s",
+ command.toString(), settings.getPortAvailableMaxTime()
+ );
+ }
+ }
+
+ private void logToFile(ProcessBuilder pb) {
+ File tempDir = new File(System.getProperty("java.io.tmpdir"));
+ Path logFile = tempDir.toPath().resolve("gasper.log");
+ pb.redirectErrorStream(true);
+ pb.redirectOutput(logFile.toFile());
+ log("Logging server messages to: %s", logFile);
+ }
+
+ private static boolean isPortTaken(int port) {
+ try (ServerSocket ignored = new ServerSocket(port)) {
+ return false;
+ } catch (IOException ex) {
+ log.trace(format("Port %d taken", port), ex);
+ return true;
+ }
+ }
+
+ private void log(String frmt, Object... args) {
+ ensureLogger();
+ logger.info(format(frmt, args));
+ }
+
+ private void ensureLogger() {
+ if (logger == null) {
+ logger = new Logger(log, settings);
+ }
+ }
+}
diff --git a/src/main/java/pl/wavesoftware/gasper/internal/HttpEndpoint.java b/src/main/java/pl/wavesoftware/gasper/internal/HttpEndpoint.java
new file mode 100644
index 0000000..261871c
--- /dev/null
+++ b/src/main/java/pl/wavesoftware/gasper/internal/HttpEndpoint.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2016 Wave Software
+ *
+ * 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 pl.wavesoftware.gasper.internal;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+
+import static java.lang.String.format;
+
+/**
+ * @author Krzysztof Suszyński
+ * @since 2016-03-05
+ */
+@Getter
+@Setter
+@RequiredArgsConstructor
+public class HttpEndpoint {
+ public static final String DEFAULT_SCHEME = "http";
+ public static final String DEFAULT_DOMAIN = "localhost";
+ public static final String DEFAULT_QUERY = null;
+
+ private final String scheme;
+ private final String domain;
+ private final int port;
+ private final String context;
+ private final String query;
+
+ public String fullAddress() {
+ String address = format("%s://%s:%s%s",
+ getScheme(),
+ getDomain(),
+ getPort(),
+ getContext()
+ );
+ if (getQuery() != null) {
+ address += "?" + getQuery();
+ }
+ return address;
+ }
+}
diff --git a/src/main/java/pl/wavesoftware/gasper/internal/Logger.java b/src/main/java/pl/wavesoftware/gasper/internal/Logger.java
new file mode 100644
index 0000000..fe37b23
--- /dev/null
+++ b/src/main/java/pl/wavesoftware/gasper/internal/Logger.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2016 Wave Software
+ *
+ * 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 pl.wavesoftware.gasper.internal;
+
+import lombok.RequiredArgsConstructor;
+import org.slf4j.event.Level;
+
+/**
+ * @author Krzysztof Suszyński
+ * @since 2016-03-05
+ */
+@RequiredArgsConstructor
+public class Logger {
+ private final org.slf4j.Logger slf;
+ private final Settings settings;
+
+ public void info(String message) {
+ if (settings.getLevel().toInt() <= Level.INFO.toInt()) {
+ slf.info(message);
+ }
+ }
+}
diff --git a/src/main/java/pl/wavesoftware/gasper/internal/Settings.java b/src/main/java/pl/wavesoftware/gasper/internal/Settings.java
new file mode 100644
index 0000000..52a4e6c
--- /dev/null
+++ b/src/main/java/pl/wavesoftware/gasper/internal/Settings.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2016 Wave Software
+ *
+ * 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 pl.wavesoftware.gasper.internal;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.slf4j.event.Level;
+import pl.wavesoftware.gasper.Gasper;
+
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+/**
+ * This class represents a set of settings for Gasper. It is used as a POJO with settings.
+ *
+ * CAUTION! It is internal class of Gasper, and shouldn't be used directly. Use gasper configure interface {@link Gasper#configure()} or {@link Gasper#configurations()} to set those settings.
+ *
+ * @author Krzysztof Suszyński
+ * @since 2016-03-05
+ * @see Gasper#configure()
+ * @see Gasper#configurations()
+ */
+@Getter
+@RequiredArgsConstructor
+public class Settings {
+ private final String packaging;
+ private final String classifier;
+ private final int port;
+ private final Map systemProperties;
+ private final List jvmOptions;
+ private final Map environment;
+ private final boolean inheritIO;
+ private final String context;
+ private final Function contextChecker;
+ private final int portAvailableMaxTime;
+ private final int deploymentMaxTime;
+ private final Path pomfile;
+ private final Level level;
+ private HttpEndpoint endpoint;
+
+ /**
+ * Retrieves Java -D
style options as map
+ * @return a map for Java options
+ */
+ public Map getSystemProperties() {
+ return ImmutableMap.copyOf(systemProperties);
+ }
+
+ /**
+ * Retrieves Java VM options as list
+ * @return a map for Java options
+ */
+ public List getJvmOptions() {
+ return ImmutableList.copyOf(jvmOptions);
+ }
+
+ /**
+ * Retrieves environment variables as a map
+ * @return a map for environment variables
+ */
+ public Map getEnvironment() {
+ return ImmutableMap.copyOf(environment);
+ }
+
+ public HttpEndpoint getEndpoint() {
+ ensureHttpEndpoint();
+ return endpoint;
+ }
+
+ private void ensureHttpEndpoint() {
+ if (endpoint == null) {
+ endpoint = new HttpEndpoint(
+ HttpEndpoint.DEFAULT_SCHEME,
+ HttpEndpoint.DEFAULT_DOMAIN,
+ port,
+ context,
+ HttpEndpoint.DEFAULT_QUERY
+ );
+ }
+ }
+}
diff --git a/src/main/java/pl/wavesoftware/gasper/internal/maven/MavenResolver.java b/src/main/java/pl/wavesoftware/gasper/internal/maven/MavenResolver.java
new file mode 100644
index 0000000..a339809
--- /dev/null
+++ b/src/main/java/pl/wavesoftware/gasper/internal/maven/MavenResolver.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2016 Wave Software
+ *
+ * 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 pl.wavesoftware.gasper.internal.maven;
+
+import org.apache.maven.model.Model;
+import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Objects;
+
+import static pl.wavesoftware.eid.utils.EidPreconditions.*;
+
+
+/**
+ * @author Krzysztof Suszyński
+ * @since 2016-03-04
+ */
+public class MavenResolver {
+
+ public static final String DEFAULT_POM = "./pom.xml";
+ public static final String DEFAULT_BUILD_DIR = "target";
+ public static final String DEFAULT_PACKAGING = "jar";
+ public static final String DEFAULT_CLASSIFIER = "";
+
+ private static final Path CURRENT_DIR = Paths.get("./");
+ private final Model model;
+ private final Path pomDirectory;
+
+ public MavenResolver() {
+ this(DEFAULT_POM);
+ }
+
+ public MavenResolver(String pomfile) {
+ this(Paths.get(pomfile));
+ }
+
+ public MavenResolver(Path pomfile) {
+ checkArgument(pomfile.toFile().isFile(), "20160305:181005");
+ MavenXpp3Reader mavenReader = new MavenXpp3Reader();
+ model = tryToExecute((UnsafeSupplier) () -> {
+ InputStream is = new FileInputStream(pomfile.toFile());
+ return mavenReader.read(is);
+ }, "20160305:203232");
+ checkNotNull(model, "20160305:203551").setPomFile(pomfile.toFile());
+ pomDirectory = pomfile.getParent() == null ? CURRENT_DIR : pomfile.getParent();
+ checkArgument(pomDirectory.toFile().isDirectory(), "20160305:181211");
+ }
+
+ public Path getBuildArtifact() {
+ return getBuildArtifact("", "");
+ }
+
+ public Path getBuildArtifact(String classifier) {
+ return getBuildArtifact("", classifier);
+ }
+
+ public Path getBuildArtifact(String packaging, String classifier) {
+ String artifact;
+ Path dir = getBuildDirectory().toPath();
+ String pack = Objects.equals(packaging, "") ? getModelPackaging() : packaging;
+ if (Objects.equals(classifier, "")) {
+ artifact = String.format("%s-%s.%s",
+ model.getArtifactId(), model.getVersion(), pack);
+ } else {
+ artifact = String.format("%s-%s-%s.%s",
+ model.getArtifactId(), model.getVersion(), classifier, pack);
+ }
+ Path artifactPath = dir.resolve(Paths.get(artifact));
+ checkState(artifactPath.toFile().isFile(), "20160305:181432", "Is not a file: %s", artifactPath);
+ checkState(artifactPath.toFile().canRead(), "20160305:181456", "Can't read file: %s", artifactPath);
+ return artifactPath;
+ }
+
+ public File getBuildDirectory() {
+ String set = model.getBuild().getOutputDirectory();
+ Path directory = pomDirectory.resolve(Paths.get(set == null ? DEFAULT_BUILD_DIR : set));
+ checkState(directory.toFile().isDirectory(), "20160304:230811");
+ return directory.normalize().toFile();
+ }
+
+ public String getModelPackaging() {
+ return model.getPackaging() == null ? DEFAULT_PACKAGING : model.getPackaging();
+ }
+}
diff --git a/src/main/resources/gasper.txt b/src/main/resources/gasper.txt
new file mode 100644
index 0000000..c95725c
--- /dev/null
+++ b/src/main/resources/gasper.txt
@@ -0,0 +1,7 @@
+
+ ____ _
+ / ___| __ _ ___ _ __ ___ _ __| |
+| | _ / _` / __| '_ \ / _ \ '__| |
+| |_| | (_| \__ \ |_) | __/ | |_| simple integration
+ \____|\__,_|___/ .__/ \___|_| (_) JUnit test harness!
+ |_|
diff --git a/src/test/java/pl/wavesoftware/gasper/GasperBuilderTest.java b/src/test/java/pl/wavesoftware/gasper/GasperBuilderTest.java
new file mode 100644
index 0000000..3d24208
--- /dev/null
+++ b/src/test/java/pl/wavesoftware/gasper/GasperBuilderTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2016 Wave Software
+ *
+ * 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 pl.wavesoftware.gasper;
+
+import com.mashape.unirest.http.Unirest;
+import org.junit.Test;
+import pl.wavesoftware.eid.utils.EidPreconditions;
+import pl.wavesoftware.gasper.internal.HttpEndpoint;
+
+import java.nio.file.Paths;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static pl.wavesoftware.eid.utils.EidPreconditions.tryToExecute;
+
+/**
+ * @author Krzysztof Suszyński
+ * @since 2016-03-05
+ */
+public class GasperBuilderTest {
+
+ @Test
+ public void testBuild() throws Exception {
+ GasperBuilder builder = new GasperBuilder();
+ int port = 11909;
+ String webContext = "/test";
+ String systemPropertyForPort = "swarm.http.port";
+ Gasper gasper = builder.silentGasperMessages()
+ .usingSystemPropertyForPort(systemPropertyForPort)
+ .withSystemProperty("swarm.context.path", webContext)
+ .withSystemProperty(systemPropertyForPort, String.valueOf(port))
+ .withJVMOptions("-server", "-Xms1G", "-Xmx1G", "-XX:+UseConcMarkSweepGC")
+ .withMaxStartupTime(100)
+ .withMaxDeploymentTime(20)
+ .withEnvironmentVariable("jdbc.password", "S3CreT!1")
+ .withTestApplicationLoggingOnConsole()
+ .usingPomFile(Paths.get("pom.xml"))
+ .withArtifactPackaging("jar")
+ .waitForWebContext(webContext)
+ .withArtifactClassifier("swarm")
+ .usingWebContextChecker(GasperBuilderTest::checkContext)
+ .withPort(port)
+ .build();
+
+ assertThat(gasper).isNotNull();
+ }
+
+ private static Boolean checkContext(HttpEndpoint endpoint) {
+ return tryToExecute((EidPreconditions.UnsafeSupplier) () ->
+ Unirest.get(endpoint.fullAddress()).asBinary().getStatus() == 200, "20160305:215916");
+ }
+}
diff --git a/src/test/java/pl/wavesoftware/gasper/GasperForSpringBootIT.java b/src/test/java/pl/wavesoftware/gasper/GasperForSpringBootIT.java
new file mode 100644
index 0000000..aa483ab
--- /dev/null
+++ b/src/test/java/pl/wavesoftware/gasper/GasperForSpringBootIT.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2016 Wave Software
+ *
+ * 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 pl.wavesoftware.gasper;
+
+import com.mashape.unirest.http.HttpResponse;
+import com.mashape.unirest.http.Unirest;
+import com.mashape.unirest.http.exceptions.UnirestException;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Krzysztof Suszyński
+ * @since 2016-03-05
+ */
+@Slf4j
+public class GasperForSpringBootIT {
+
+ private static final Path SPRING_BOOT_POMFILE = Paths.get(
+ "target", "it", "spring-boot-tester", "pom.xml"
+ );
+
+ @ClassRule
+ public static Gasper gasper = Gasper.configurations()
+ .springBoot()
+ .usingPomFile(SPRING_BOOT_POMFILE)
+ .build();
+
+ @Test
+ public void testGetRoot() throws UnirestException {
+ // given
+ String address = gasper.getAddress();
+ String expectedMessage = "Hello from Spring Boot!";
+
+ // when
+ HttpResponse response = Unirest.get(address).asString();
+
+ // then
+ assertThat(response.getStatus()).isEqualTo(200);
+ assertThat(response.getBody()).isEqualTo(expectedMessage);
+ log.info("Server returned: " + response.getBody());
+ }
+
+ @Test
+ public void testGetNonExistent() throws UnirestException {
+ // given
+ String nonExistingPath = "non-existing";
+ String address = gasper.getAddress() + nonExistingPath;
+
+ // when
+ HttpResponse response = Unirest.get(address).asString();
+
+ // then
+ assertThat(response.getStatus()).isEqualTo(404);
+ }
+
+}
diff --git a/src/test/java/pl/wavesoftware/gasper/GasperForWildflySwarmIT.java b/src/test/java/pl/wavesoftware/gasper/GasperForWildflySwarmIT.java
new file mode 100644
index 0000000..a0f8385
--- /dev/null
+++ b/src/test/java/pl/wavesoftware/gasper/GasperForWildflySwarmIT.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2016 Wave Software
+ *
+ * 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 pl.wavesoftware.gasper;
+
+import com.mashape.unirest.http.HttpResponse;
+import com.mashape.unirest.http.Unirest;
+import com.mashape.unirest.http.exceptions.UnirestException;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Krzysztof Suszyński
+ * @since 2016-03-05
+ */
+public class GasperForWildflySwarmIT {
+
+ private static final Path WILDFLY_SWARM_POMFILE = Paths.get(
+ "target", "it", "wildfly-swarm-tester", "pom.xml"
+ );
+
+ @ClassRule
+ public static Gasper gasper = Gasper.configurations()
+ .wildflySwarm()
+ .usingPomFile(WILDFLY_SWARM_POMFILE)
+ .silentGasperMessages()
+ .build();
+
+ @Test
+ public void testGetRoot() throws UnirestException {
+ // given
+ String address = gasper.getAddress();
+ String expectedMessage = "Hello from WildFly Swarm!";
+
+ // when
+ HttpResponse response = Unirest.get(address).asString();
+
+ // then
+ assertThat(response.getStatus()).isEqualTo(200);
+ assertThat(response.getBody()).isEqualTo(expectedMessage);
+ assertThat(gasper.getPort()).isGreaterThanOrEqualTo(1000);
+ }
+
+}
diff --git a/src/test/java/pl/wavesoftware/gasper/internal/HttpEndpointTest.java b/src/test/java/pl/wavesoftware/gasper/internal/HttpEndpointTest.java
new file mode 100644
index 0000000..819edf8
--- /dev/null
+++ b/src/test/java/pl/wavesoftware/gasper/internal/HttpEndpointTest.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2016 Wave Software
+ *
+ * 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 pl.wavesoftware.gasper.internal;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Krzysztof Suszyński
+ * @since 2016-03-05
+ */
+public class HttpEndpointTest {
+
+ @Test
+ public void testFullAddress() throws Exception {
+ HttpEndpoint endpoint = new HttpEndpoint("http", "example.org", 8080, "/", "a=7");
+ String address = endpoint.fullAddress();
+ assertThat(address).isEqualTo("http://example.org:8080/?a=7");
+ }
+}
diff --git a/src/test/java/pl/wavesoftware/gasper/internal/maven/MavenResolverIT.java b/src/test/java/pl/wavesoftware/gasper/internal/maven/MavenResolverIT.java
new file mode 100644
index 0000000..18a20db
--- /dev/null
+++ b/src/test/java/pl/wavesoftware/gasper/internal/maven/MavenResolverIT.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2016 Wave Software
+ *
+ * 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 pl.wavesoftware.gasper.internal.maven;
+
+import org.junit.Test;
+
+import java.io.File;
+import java.nio.file.Path;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Krzysztof Suszyński
+ * @since 2016-03-04
+ */
+public class MavenResolverIT {
+
+ @Test
+ public void testGetBuildArtifact() throws Exception {
+ // given
+ MavenResolver resolver = new MavenResolver();
+
+ // when
+ Path artifact = resolver.getBuildArtifact("jar", "");
+
+ // then
+ assertThat(artifact).exists().isRegularFile();
+ }
+
+ @Test
+ public void testGetBuildArtifactForOtherPom() throws Exception {
+ // given
+ MavenResolver resolver = new MavenResolver("pom.xml");
+
+ // when
+ Path artifact = resolver.getBuildArtifact();
+
+ // then
+ assertThat(artifact).exists().isRegularFile();
+ }
+
+ @Test
+ public void testGetBuildDirectory() throws Exception {
+ // given
+ MavenResolver resolver = new MavenResolver();
+
+ // when
+ File directory = resolver.getBuildDirectory();
+
+ // then
+ assertThat(directory.toString()).isEqualTo("target");
+ }
+}