diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..962eb50 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 4 diff --git a/.gitignore b/.gitignore index 1cdc9f7..61eefef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +# Common Maven + target/ pom.xml.tag pom.xml.releaseBackup @@ -7,3 +9,7 @@ release.properties dependency-reduced-pom.xml buildNumber.properties .mvn/timing.properties + +# IDE's +.idea +*.iml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..aa3f4b1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +--- +language: java +install: mvn clean install -DskipTests=true -Dmaven.javadoc.skip=true -Dcoveralls.skip=true -B -V +script: mvn verify -Pci +jdk: + - oraclejdk8 +cache: + directories: + - $HOME/.m2 diff --git a/LICENSE b/LICENSE index 8dada3e..dc56359 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright {yyyy} {name of copyright owner} + Copyright 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6208178 --- /dev/null +++ b/README.md @@ -0,0 +1,149 @@ +# Gasper! + +[![Build Status](https://travis-ci.org/wavesoftware/java-gasper.svg?branch=develop)](https://travis-ci.org/wavesoftware/java-gasper) [![Coverage Status](https://coveralls.io/repos/github/wavesoftware/java-gasper/badge.svg?branch=develop)](https://coveralls.io/github/wavesoftware/java-gasper?branch=develop) [![Codacy Badge](https://api.codacy.com/project/badge/grade/5c4d1180812e438ebe872f9121ec4368)](https://www.codacy.com/app/krzysztof-suszynski/java-gasper) [![SonarQube Tech Debt](https://img.shields.io/sonar/http/sonar-ro.wavesoftware.pl/pl.wavesoftware:gasper/tech_debt.svg)](https://sonar.wavesoftware.pl/dashboard/index/2858) + +Gasper is a very simple integration testing JUnit harness for `java -jar` servers like [WildFly Swarm](http://wildfly-swarm.io/) and [Spring Boot](http://projects.spring.io/spring-boot/). + +[![WildFly Swarm](https://avatars3.githubusercontent.com/u/11523816?v=3&s=100)](http://wildfly-swarm.io/) [![Spring Boot](https://avatars2.githubusercontent.com/u/317776?v=3&s=100)](http://projects.spring.io/spring-boot/) + +Gasper provides a simple to use JUnit `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. + +Gasper supports currently only [Maven](https://maven.apache.org/). The `pom.xml` file is used to read project configuration achieving zero configuration operation. + +## Usage + + +Gasper utilize your packaged application. It It means it should be used in integration tests that run after application is being packaged by build tool (Maven). Add this code to your `pom.xml` file (if you didn't done that before): + +```xml + +[..] + +[..] + + org.apache.maven.plugins + maven-failsafe-plugin + 2.19.1 + + + + integration-test + verify + + + + +[..] + +[..] + +``` + + +Place your integration tests in classes that ends with `*IT` or `*ITest`. + +### WildFly Swarm configuration + +```java +@ClassRule +public static Gasper gasper = Gasper.configurations() + .wildflySwarm() + .build(); +``` + +### Spring Boot configuration + +```java +@ClassRule +public static Gasper gasper = Gasper.configurations() + .springBoot() + .build(); +``` + +Before running `GasperBuilder.build()` method, you can reconfigure those default configurations to your needs. + +### Example test method (Unirest + JSONAssert) + +Gasper is best to use with libraries like [Unirest](http://unirest.io/java.html) for fetching data and asserting HTTP/S statuses and [JSON Assert](https://github.com/marcingrzejszczak/jsonassert) to validate correctness of JSON output for REST services. + +```java +@Test +public void testGetRoot() throws UnirestException { + // given + String address = gasper.getAddress(); // Address to deployed app, running live on random port + String expectedMessage = "WildFly Swarm!"; + + // when + HttpResponse response = Unirest.get(address).asString(); + + // then + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getBody()).field("hello").isEqualTo(expectedMessage); // JSON Assert +} +``` + +### Additional configuration + +To configure Gasper use `GasperBuilder` interface, for ex.: + +```java +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(); +``` + +## Installation + +### Maven + +```xml + + pl.wavesoftware + gasper + 1.0.0 + test + +``` + +## Contributing + +Contributions are welcome! + +To contribute, follow the standard [git flow](http://danielkummer.github.io/git-flow-cheatsheet/) of: + +1. Fork it +1. Create your feature branch (`git checkout -b feature/my-new-feature`) +1. Commit your changes (`git commit -am 'Add some feature'`) +1. Push to the branch (`git push origin feature/my-new-feature`) +1. Create new Pull Request + +Even if you can't contribute code, if you have an idea for an improvement please open an [issue](https://github.com/wavesoftware/java-gasper/issues). + +## Requirements + +* Java 8 +* Maven 3 + +## Releases + +* `1.0.0` - codename: *SkyMango* + * First publicly available release diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..ae0c4f0 --- /dev/null +++ b/pom.xml @@ -0,0 +1,323 @@ + + + 4.0.0 + pl.wavesoftware + gasper + 1.0.0 + jar + Gasper + Very simple integration testing JUnit harness for 'java -jar' servers like WildFly Swarm and Spring Boot + + + + apache20 + 3.0.4 + UTF-8 + UTF-8 + ${project.build.directory}/sonar + https://sonar.wavesoftware.pl + jacoco + 8 + ${sonar.java.source} + 1.${java.source.version} + ${maven.compiler.source} + + + http://wavesoftware.github.io/java-gasper/ + + + + cardil + Krzysztof Suszyński + krzysztof.suszynski@wavesoftware.pl + Wave Software + http://wavesoftware.pl/ + + + + + Wave Software + http://wavesoftware.pl/ + + + + + Apache License 2.0 + http://www.apache.org/licenses/LICENSE-2.0 + repo + + + + + scm:git:https://github.com/wavesoftware/java-gasper.git + scm:git:git@github.com:wavesoftware/java-gasper.git + https://github.com/wavesoftware/java-gasper + + + + travis-ci + https://travis-ci.org/wavesoftware/java-gasper + + + + ${maven.required.version} + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + org.projectlombok + lombok + 1.16.6 + provided + + + + + junit + junit + 4.12 + + + com.mashape.unirest + unirest-java + 1.4.7 + + + pl.wavesoftware + eid-exceptions + 1.1.0 + + + org.slf4j + slf4j-api + 1.7.18 + + + com.google.guava + guava + 19.0 + + + + org.apache.maven + maven-model + ${maven.required.version} + + + org.apache.maven + maven-core + ${maven.required.version} + + + + org.mockito + mockito-core + 2.0.43-beta + test + + + org.assertj + assertj-core + 3.3.0 + test + + + org.slf4j + slf4j-simple + 1.6.6 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.3 + + + -Xlint:-deprecation + -Xlint:all + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.19.1 + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.19.1 + + + + integration-test + verify + + + + + + org.apache.maven.plugins + maven-invoker-plugin + 2.0.0 + + + prepare-packages + + run + + package + + + + ${project.build.directory}/it + 3 + + + + external.atlassian.jgitflow + jgitflow-maven-plugin + 1.0-m5.1 + + true + + v + + + + + + + + + release-profile + + + performRelease + true + + + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.5 + + + sign-artifacts + verify + + sign + + + + + + + + + ci + + + + + org.jacoco + jacoco-maven-plugin + 0.7.5.201505241946 + + + jacoco-initialize + + prepare-agent + prepare-agent-integration + + + + jacoco-site + post-integration-test + + report + report-integration + + + + + + pl/wavesoftware/** + + + + + + + + travis + + + env.TRAVIS + true + + + + + + org.eluder.coveralls + coveralls-maven-plugin + 4.1.0 + + + coveralls-default + verify + + report + + + + + + + + + diff --git a/src/it/spark-tester/pom.xml b/src/it/spark-tester/pom.xml new file mode 100644 index 0000000..eeeae73 --- /dev/null +++ b/src/it/spark-tester/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + pl.wavesoftware.examples + spark-tester + 1.0.0 + jar + Spark Tester app + + + 1.8 + 1.8 + UTF-8 + UTF-8 + + + + + com.sparkjava + spark-core + 2.3 + + + org.slf4j + slf4j-simple + 1.6.2 + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.6 + + + org.apache.maven.plugins + maven-shade-plugin + 2.4.3 + + + package + + shade + + + + + pl.wavesoftware.examples.sparktester.HelloApp + + + false + + + + + + + diff --git a/src/it/spark-tester/src/main/java/pl/wavesoftware/examples/sparktester/HelloApp.java b/src/it/spark-tester/src/main/java/pl/wavesoftware/examples/sparktester/HelloApp.java new file mode 100644 index 0000000..1a09eed --- /dev/null +++ b/src/it/spark-tester/src/main/java/pl/wavesoftware/examples/sparktester/HelloApp.java @@ -0,0 +1,9 @@ +package pl.wavesoftware.examples.sparktester; + +import static spark.Spark.*; + +public class HelloApp { + public static void main(String[] args) { + get("/", (req, res) -> "Hello from Spark!"); + } +} diff --git a/src/it/spring-boot-tester/pom.xml b/src/it/spring-boot-tester/pom.xml new file mode 100644 index 0000000..802c3f5 --- /dev/null +++ b/src/it/spring-boot-tester/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + pl.wavesoftware.examples + spring-boot-tester + 1.0.0 + jar + Spring Boot Tester app + + + org.springframework.boot + spring-boot-starter-parent + 1.3.3.RELEASE + + + + + org.springframework.boot + spring-boot-starter-web + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/src/it/spring-boot-tester/src/main/java/pl/wavesoftware/examples/springboottester/SampleController.java b/src/it/spring-boot-tester/src/main/java/pl/wavesoftware/examples/springboottester/SampleController.java new file mode 100644 index 0000000..5cd0482 --- /dev/null +++ b/src/it/spring-boot-tester/src/main/java/pl/wavesoftware/examples/springboottester/SampleController.java @@ -0,0 +1,21 @@ +package pl.wavesoftware.examples.springboottester; + +import org.springframework.boot.*; +import org.springframework.boot.autoconfigure.*; +import org.springframework.stereotype.*; +import org.springframework.web.bind.annotation.*; + +@Controller +@EnableAutoConfiguration +public class SampleController { + + @RequestMapping("/") + @ResponseBody + String home() { + return "Hello from Spring Boot!"; + } + + public static void main(String[] args) throws Exception { + SpringApplication.run(SampleController.class, args); + } +} diff --git a/src/it/wildfly-swarm-tester/pom.xml b/src/it/wildfly-swarm-tester/pom.xml new file mode 100644 index 0000000..35167e8 --- /dev/null +++ b/src/it/wildfly-swarm-tester/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + pl.wavesoftware.examples + wildfly-swarm-tester + 1.0.0 + war + WildFly Swarm Tester app + + + 3.2.5 + + + + 1.8 + 1.8 + UTF-8 + UTF-8 + 1.0.0.Beta2 + + + + + org.wildfly.swarm + weld + ${wildfly.swarm.version} + provided + + + org.wildfly.swarm + jsf + ${wildfly.swarm.version} + provided + + + + + + + org.apache.maven.plugins + maven-war-plugin + 2.5 + + true + + + + org.wildfly.swarm + wildfly-swarm-plugin + ${wildfly.swarm.version} + + + package + + package + + + + + + + diff --git a/src/it/wildfly-swarm-tester/src/main/java/pl/wavesoftware/examples/wildflyswarmtester/HelloServlet.java b/src/it/wildfly-swarm-tester/src/main/java/pl/wavesoftware/examples/wildflyswarmtester/HelloServlet.java new file mode 100644 index 0000000..5d8e226 --- /dev/null +++ b/src/it/wildfly-swarm-tester/src/main/java/pl/wavesoftware/examples/wildflyswarmtester/HelloServlet.java @@ -0,0 +1,21 @@ +package pl.wavesoftware.examples.wildflyswarmtester; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author Krzysztof Suszynski + * @since 04.03.16 + */ +@WebServlet("/") +public class HelloServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.getWriter().append("Hello from WildFly Swarm!"); + } +} diff --git a/src/it/wildfly-swarm-tester/src/main/webapp/WEB-INF/beans.xml b/src/it/wildfly-swarm-tester/src/main/webapp/WEB-INF/beans.xml new file mode 100644 index 0000000..1f19832 --- /dev/null +++ b/src/it/wildfly-swarm-tester/src/main/webapp/WEB-INF/beans.xml @@ -0,0 +1,6 @@ + + + diff --git a/src/it/wildfly-swarm-tester/src/main/webapp/WEB-INF/web.xml b/src/it/wildfly-swarm-tester/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..82f55e5 --- /dev/null +++ b/src/it/wildfly-swarm-tester/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,3 @@ + + + diff --git a/src/main/java/pl/wavesoftware/gasper/Gasper.java b/src/main/java/pl/wavesoftware/gasper/Gasper.java new file mode 100644 index 0000000..e34c719 --- /dev/null +++ b/src/main/java/pl/wavesoftware/gasper/Gasper.java @@ -0,0 +1,301 @@ +/* + * 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.google.common.base.Charsets; +import com.google.common.io.CharStreams; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import pl.wavesoftware.eid.utils.EidPreconditions; +import pl.wavesoftware.gasper.internal.Executor; +import pl.wavesoftware.gasper.internal.Logger; +import pl.wavesoftware.gasper.internal.Settings; +import pl.wavesoftware.gasper.internal.maven.MavenResolver; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static java.lang.String.format; +import static pl.wavesoftware.eid.utils.EidPreconditions.tryToExecute; + +/** + *

About

+ * Gasper is a very simple integration testing JUnit harness for java -jar servers like WildFly Swarm and Spring Boot. + *

+ * 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. + *

+ * Gasper supports currently only Maven. The pom.xml file is used to read project configuration achieving zero configuration operation. + * + *

Usage

+ * + * Gasper utilize your packaged application. It It means it should be used in integration tests that run after application is being packaged by build tool (Maven). Add this code to your pom.xml file (if you didn't done that before): + * + *
+ * <build>
+ * [..]
+ * <plugins>
+ * [..]
+ * <plugin>
+ *   <groupId>org.apache.maven.plugins</groupId>
+ *   <artifactId>maven-failsafe-plugin</artifactId>
+ *   <version>2.19.1</version>
+ *   <executions>
+ *     <execution>
+ *       <goals>
+ *         <goal>integration-test</goal>
+ *         <goal>verify</goal>
+ *       </goals>
+ *     </execution>
+ *   </executions>
+ * </plugin>
+ * [..]
+ * </plugins>
+ * [..]
+ * </build>
+ * 
+ * + * Place your integration tests in classes that ends with *IT or *ITest. + * + *

WildFly Swarm default configuration

+ * + *
+ * @ClassRule
+ * public static Gasper gasper = Gasper.configurations()
+ *   .wildflySwarm()
+ *   .build();
+ * 
+ * + *

Spring Boot default configuration

+ * + *
+ * @ClassRule
+ * public static Gasper gasper = Gasper.configurations()
+ *   .springBoot()
+ *   .build();
+ * 
+ *

+ * Before running {@link GasperBuilder#build()} method, you can reconfigure those default configurations to your needs. + * + *

Example test method (Unirest + JSONAssert)

+ * + * Gasper is best to use with libraries like Unirest for fetching + * data and asserting HTTP/S statuses and JSON + * Assert to validate correctness of JSON output for REST services. + * + *
+ * @Test
+ * public void testGetRoot() throws UnirestException {
+ *   // given
+ *   String address = gasper.getAddress(); // Address to deployed app, running live on random port
+ *   String expectedMessage = "WildFly Swarm!";
+ *   // when
+ *   HttpResponse<String> response = Unirest.get(address).asString();
+ *   // then
+ *   assertThat(response.getStatus()).isEqualTo(200);
+ *   assertThat(response.getBody()).field("hello").isEqualTo(expectedMessage); // JSON Assert
+ * }
+ * 
+ * + *

Additional configuration

+ * + * To configure Gasper use {@link GasperBuilder} interface, for ex.: + * + *
+ * 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();
+ * 
+ * + *

Requirements

+ * + * + * + * @author Krzysztof Suszyński <krzysztof suszynski@wavesoftware.pl> + * @since 2016-03-04 + */ +@Slf4j +@RequiredArgsConstructor(access = AccessLevel.PROTECTED) +public final class Gasper implements TestRule { + + public static final int DEFAULT_PORT_AVAILABLE_MAX_SECONDS = 60; + public static final int DEFAULT_DEPLOYMENT_MAX_SECONDS = 30; + public static final String DEFAULT_CONTEXT = "/"; + private static final String FIGLET; + + private final Settings settings; + private Path artifact; + private Executor executor; + private Logger logger; + + static { + InputStream is = Gasper.class.getClassLoader().getResourceAsStream("gasper.txt"); + FIGLET = tryToExecute((EidPreconditions.UnsafeSupplier) () -> + 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"); + } +}