diff --git a/pom.xml b/pom.xml index 767854e32..55bf6f32c 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,5 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.springframework.data.examples @@ -16,6 +16,7 @@ + util bom couchbase elasticsearch @@ -36,6 +37,7 @@ 1.1.3 1.18.0 + 1.9.1 diff --git a/r2dbc/example/pom.xml b/r2dbc/example/pom.xml index edf96a7a7..4b4abbe2b 100644 --- a/r2dbc/example/pom.xml +++ b/r2dbc/example/pom.xml @@ -13,4 +13,11 @@ Spring Data R2DBC - Example + + + org.springframework.data.examples + spring-data-examples-utils + ${project.version} + + diff --git a/r2dbc/example/src/test/java/example/springdata/r2dbc/basics/CustomerRepositoryIntegrationTests.java b/r2dbc/example/src/test/java/example/springdata/r2dbc/basics/CustomerRepositoryIntegrationTests.java index b80692916..a83edae39 100644 --- a/r2dbc/example/src/test/java/example/springdata/r2dbc/basics/CustomerRepositoryIntegrationTests.java +++ b/r2dbc/example/src/test/java/example/springdata/r2dbc/basics/CustomerRepositoryIntegrationTests.java @@ -15,6 +15,7 @@ */ package example.springdata.r2dbc.basics; +import example.springdata.test.util.InfrastructureRule; import reactor.core.publisher.Hooks; import reactor.test.StepVerifier; @@ -23,6 +24,7 @@ import java.util.List; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -31,7 +33,10 @@ import org.springframework.test.context.junit4.SpringRunner; /** + * Tests demonstrating the use of R2DBC. + * * @author Oliver Gierke + * @author Jens Schauder */ @RunWith(SpringRunner.class) @SpringBootTest(classes = InfrastructureConfiguration.class) @@ -40,6 +45,8 @@ public class CustomerRepositoryIntegrationTests { @Autowired CustomerRepository customers; @Autowired DatabaseClient database; + @Rule @Autowired public InfrastructureRule requiresPostgres; + @Before public void setUp() { diff --git a/r2dbc/example/src/test/java/example/springdata/r2dbc/basics/InfrastructureConfiguration.java b/r2dbc/example/src/test/java/example/springdata/r2dbc/basics/InfrastructureConfiguration.java index 94e29bd6b..9b0e9fda0 100644 --- a/r2dbc/example/src/test/java/example/springdata/r2dbc/basics/InfrastructureConfiguration.java +++ b/r2dbc/example/src/test/java/example/springdata/r2dbc/basics/InfrastructureConfiguration.java @@ -15,18 +15,19 @@ */ package example.springdata.r2dbc.basics; +import example.springdata.test.util.InfrastructureRule; import io.r2dbc.postgresql.PostgresqlConnectionConfiguration; import io.r2dbc.postgresql.PostgresqlConnectionFactory; import io.r2dbc.spi.ConnectionFactory; +import org.jetbrains.annotations.NotNull; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactory; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.testcontainers.containers.PostgreSQLContainer; - -import javax.annotation.PreDestroy; +import reactor.test.StepVerifier; /** * @author Oliver Gierke @@ -34,7 +35,14 @@ @Configuration class InfrastructureConfiguration { - private PostgreSQLContainer postgres = new PostgreSQLContainer(); + @Bean + InfrastructureRule infrastructureRule() { + + return new InfrastructureRule<>( // + this::checkForlLocalPostgres, // + this::startPostgresInDocker // + ); + } @Bean CustomerRepository customerRepository(R2dbcRepositoryFactory factory) { @@ -61,10 +69,16 @@ DatabaseClient databaseClient(ConnectionFactory factory) { @Bean PostgresqlConnectionFactory connectionFactory() { + return new PostgresqlConnectionFactory(infrastructureRule().getInfo()); + } + + @NotNull + private InfrastructureRule.InfrastructureInfo startPostgresInDocker() { + PostgreSQLContainer postgres = new PostgreSQLContainer(); postgres.start(); - PostgresqlConnectionConfiguration config = PostgresqlConnectionConfiguration.builder() // + PostgresqlConnectionConfiguration configuration = PostgresqlConnectionConfiguration.builder() // .host(postgres.getContainerIpAddress()) // .port(postgres.getFirstMappedPort()) // .database(postgres.getDatabaseName()) // @@ -72,11 +86,33 @@ PostgresqlConnectionFactory connectionFactory() { .password(postgres.getPassword()) // .build(); - return new PostgresqlConnectionFactory(config); + return new InfrastructureRule.InfrastructureInfo<>(true, configuration, null, postgres::stop); } - @PreDestroy - void shutdown() { - postgres.stop(); + @NotNull + private InfrastructureRule.InfrastructureInfo checkForlLocalPostgres() { + + PostgresqlConnectionConfiguration configuration = PostgresqlConnectionConfiguration.builder() // + .host("localhost") // + .port(5432) // + .database("postgres") // + .username("postgres") // + .password("") // + .build(); + + try { + + new PostgresqlConnectionFactory(configuration).create() + .as(StepVerifier::create) // + .assertNext(c -> { + }) // + .verifyComplete(); + } catch (AssertionError re) { + + return new InfrastructureRule.InfrastructureInfo<>(false, null, re, () ->{}); + } + + return new InfrastructureRule.InfrastructureInfo<>(true, configuration, null, () ->{}); } + } diff --git a/solr/example/pom.xml b/solr/example/pom.xml index 1028e6d89..cb3ebfecd 100644 --- a/solr/example/pom.xml +++ b/solr/example/pom.xml @@ -18,6 +18,17 @@ ${project.version} test + + org.springframework.data.examples + spring-data-examples-utils + ${project.version} + + + org.testcontainers + testcontainers + ${testcontainers.version} + test + diff --git a/solr/example/src/main/java/example/springdata/solr/product/Product.java b/solr/example/src/main/java/example/springdata/solr/product/Product.java index cc22ea1a9..e04af7d64 100644 --- a/solr/example/src/main/java/example/springdata/solr/product/Product.java +++ b/solr/example/src/main/java/example/springdata/solr/product/Product.java @@ -43,7 +43,7 @@ public class Product { private @Indexed(name = "cat") List category; private @Indexed(name = "store") Point location; private @Indexed String description; - private @Indexed boolean inStock; + private @Indexed Boolean inStock; private @Indexed Integer popularity; private @Score Float score; } diff --git a/solr/example/src/test/java/example/springdata/solr/AdvancedSolrRepositoryTests.java b/solr/example/src/test/java/example/springdata/solr/AdvancedSolrRepositoryTests.java index 0a4ec5c27..29ba05b81 100644 --- a/solr/example/src/test/java/example/springdata/solr/AdvancedSolrRepositoryTests.java +++ b/solr/example/src/test/java/example/springdata/solr/AdvancedSolrRepositoryTests.java @@ -22,20 +22,20 @@ import example.springdata.solr.product.Product; import example.springdata.solr.product.ProductRepository; -import example.springdata.solr.test.util.RequiresSolrServer; +import example.springdata.test.util.InfrastructureRule; import java.time.Duration; import java.util.Arrays; import java.util.Optional; -import org.junit.ClassRule; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Configuration; import org.springframework.data.domain.PageRequest; -import org.springframework.data.repository.CrudRepository; import org.springframework.data.solr.core.SolrOperations; import org.springframework.data.solr.core.query.Function; import org.springframework.data.solr.core.query.Query; @@ -48,33 +48,41 @@ * @author Christoph Strobl * @author Oliver Gierke * @author Mark Paluch + * @author Jens Schauder */ @RunWith(SpringRunner.class) @SpringBootTest public class AdvancedSolrRepositoryTests { - public static @ClassRule RequiresSolrServer requiresRunningServer = RequiresSolrServer.onLocalhost(); + @Rule @Autowired public InfrastructureRule requiresRunningServer; - @Configuration - static class Config extends SolrTestConfiguration { - - @Override - protected void doInitTestData(CrudRepository repository) { + @Autowired ProductRepository repository; + @Autowired SolrOperations operations; - Product playstation = Product.builder().id("id-1").name("Playstation") - .description("The Sony playstation was the top selling gaming system in 1994.").popularity(5).build(); - Product playstation2 = Product.builder().id("id-2").name("Playstation Two") - .description("Playstation two is the successor of playstation in 2000.").build(); - Product superNES = Product.builder().id("id-3").name("Super Nintendo").popularity(3).build(); - Product nintendo64 = Product.builder().id("id-4").name("N64").description("Nintendo 64").popularity(2).build(); - repository.saveAll(Arrays.asList(playstation, playstation2, superNES, nintendo64)); - } + /** + * Remove test data when context is shut down. + */ + public @After void deleteDocumentsOnShutdown() { + repository.deleteAll(); } - @Autowired ProductRepository repository; - @Autowired SolrOperations operations; + /** + * Initialize Solr instance with test data once context has started. + */ + public @Before void initWithTestData() throws InterruptedException { + repository.deleteAll(); + + Product playstation = Product.builder().id("id-1").name("Playstation") + .description("The Sony playstation was the top selling gaming system in 1994.").popularity(5).build(); + Product playstation2 = Product.builder().id("id-2").name("Playstation Two") + .description("Playstation two is the successor of playstation in 2000.").build(); + Product superNES = Product.builder().id("id-3").name("Super Nintendo").popularity(3).build(); + Product nintendo64 = Product.builder().id("id-4").name("N64").description("Nintendo 64").popularity(2).build(); + + repository.saveAll(Arrays.asList(playstation, playstation2, superNES, nintendo64)); + } /** * {@link HighlightPage} holds next to the entities found also information about where a match was found within the * document. This allows to fine grained display snipplets of data containing the matching term in context. diff --git a/solr/example/src/test/java/example/springdata/solr/BasicSolrRepositoryTests.java b/solr/example/src/test/java/example/springdata/solr/BasicSolrRepositoryTests.java index e06293b7e..84db2d0a6 100644 --- a/solr/example/src/test/java/example/springdata/solr/BasicSolrRepositoryTests.java +++ b/solr/example/src/test/java/example/springdata/solr/BasicSolrRepositoryTests.java @@ -15,10 +15,15 @@ */ package example.springdata.solr; +import example.springdata.solr.product.Product; import example.springdata.solr.product.ProductRepository; -import example.springdata.solr.test.util.RequiresSolrServer; +import example.springdata.test.util.InfrastructureRule; -import org.junit.ClassRule; +import java.util.stream.IntStream; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -27,15 +32,36 @@ /** * @author Christoph Strobl + * @author Jens Schauder */ @RunWith(SpringRunner.class) -@SpringBootTest(classes = SolrTestConfiguration.class) +@SpringBootTest public class BasicSolrRepositoryTests { - public static @ClassRule RequiresSolrServer requiresRunningServer = RequiresSolrServer.onLocalhost(); + + + @Rule @Autowired public InfrastructureRule requiresRunningServer; @Autowired ProductRepository repository; + /** + * Remove test data when context is shut down. + */ + public @After void deleteDocumentsOnShutdown() { + repository.deleteAll(); + } + + /** + * Initialize Solr instance with test data once context has started. + */ + public @Before void initWithTestData() { + + repository.deleteAll(); + + IntStream.range(0, 100) + .forEach(index -> repository.save(Product.builder().id("p-" + index).name("foobar").build())); + } + /** * Finds all entries using a single request. */ diff --git a/solr/example/src/test/java/example/springdata/solr/SolrTestConfiguration.java b/solr/example/src/test/java/example/springdata/solr/SolrTestConfiguration.java index 079c81a28..186619a32 100644 --- a/solr/example/src/test/java/example/springdata/solr/SolrTestConfiguration.java +++ b/solr/example/src/test/java/example/springdata/solr/SolrTestConfiguration.java @@ -16,13 +16,12 @@ package example.springdata.solr; import example.springdata.solr.product.Product; - -import java.util.stream.IntStream; - -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; +import example.springdata.solr.test.util.SolrInfrastructureRule; +import example.springdata.test.util.InfrastructureRule; import org.apache.solr.client.solrj.impl.HttpSolrClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @@ -32,33 +31,22 @@ /** * @author Christoph Strobl * @author Oliver Gierke + * @author Jens Schauder */ @SpringBootApplication public class SolrTestConfiguration { @Autowired CrudRepository repo; - public @Bean SolrTemplate solrTemplate() { - return new SolrTemplate(new HttpSolrClient.Builder().withBaseSolrUrl("http://localhost:8983/solr").build()); - } + private static final Logger LOG = LoggerFactory.getLogger(SolrTestConfiguration.class); - /** - * Remove test data when context is shut down. - */ - public @PreDestroy void deleteDocumentsOnShutdown() { - repo.deleteAll(); - } + public @Bean InfrastructureRule infrastructureRule() { - /** - * Initialize Solr instance with test data once context has started. - */ - public @PostConstruct void initWithTestData() { - doInitTestData(repo); + return new SolrInfrastructureRule("techproducts"); } - protected void doInitTestData(CrudRepository repository) { - - IntStream.range(0, 100) - .forEach(index -> repository.save(Product.builder().id("p-" + index).name("foobar").build())); + public @Bean SolrTemplate solrTemplate() { + return new SolrTemplate(new HttpSolrClient.Builder().withBaseSolrUrl(infrastructureRule().getInfo()).build()); } + } diff --git a/solr/managed-schema/src/test/java/example/springdata/solr/SolrRepositoryTests.java b/solr/managed-schema/src/test/java/example/springdata/solr/SolrRepositoryTests.java index f4ce8e79f..866106e77 100644 --- a/solr/managed-schema/src/test/java/example/springdata/solr/SolrRepositoryTests.java +++ b/solr/managed-schema/src/test/java/example/springdata/solr/SolrRepositoryTests.java @@ -17,9 +17,9 @@ import example.springdata.solr.product.ManagedProduct; import example.springdata.solr.product.ProductRepository; -import example.springdata.solr.test.util.RequiresSolrServer; +import example.springdata.test.util.InfrastructureRule; -import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -28,12 +28,13 @@ /** * @author Christoph Strobl + * @author Jens Schauder */ @RunWith(SpringRunner.class) @SpringBootTest public class SolrRepositoryTests { - public static @ClassRule RequiresSolrServer requiresRunningServer = RequiresSolrServer.onLocalhost(); + @Rule @Autowired public InfrastructureRule requiresRunningServer; @Autowired ProductRepository repo; diff --git a/solr/managed-schema/src/test/java/example/springdata/solr/SolrTestConfiguration.java b/solr/managed-schema/src/test/java/example/springdata/solr/SolrTestConfiguration.java index f7fd03966..7c2f8bc65 100644 --- a/solr/managed-schema/src/test/java/example/springdata/solr/SolrTestConfiguration.java +++ b/solr/managed-schema/src/test/java/example/springdata/solr/SolrTestConfiguration.java @@ -19,6 +19,8 @@ import javax.annotation.PreDestroy; +import example.springdata.solr.test.util.SolrInfrastructureRule; +import example.springdata.test.util.InfrastructureRule; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.impl.HttpSolrClient; import org.springframework.beans.factory.annotation.Autowired; @@ -41,8 +43,13 @@ public class SolrTestConfiguration { @Autowired ProductRepository repo; + public @Bean + InfrastructureRule infrastructureRule() { + return new SolrInfrastructureRule("schemaless"); + } + public @Bean SolrClient solrClient() { - return new HttpSolrClient.Builder().withBaseSolrUrl("http://localhost:8983/solr").build(); + return new HttpSolrClient.Builder().withBaseSolrUrl(infrastructureRule().getInfo()).build(); } /** diff --git a/solr/util/pom.xml b/solr/util/pom.xml index 9be802f0d..6d7c6ea99 100644 --- a/solr/util/pom.xml +++ b/solr/util/pom.xml @@ -16,5 +16,15 @@ junit junit + + org.testcontainers + testcontainers + 1.7.3 + + + org.springframework.data.examples + spring-data-examples-utils + ${project.version} + diff --git a/solr/util/src/main/java/example/springdata/solr/test/util/RequiresSolrServer.java b/solr/util/src/main/java/example/springdata/solr/test/util/RequiresSolrServer.java deleted file mode 100644 index ee36bfd29..000000000 --- a/solr/util/src/main/java/example/springdata/solr/test/util/RequiresSolrServer.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2014-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package example.springdata.solr.test.util; - -import java.io.IOException; - -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import org.hamcrest.core.Is; -import org.junit.Assume; -import org.junit.AssumptionViolatedException; -import org.junit.rules.TestRule; -import org.junit.runner.Description; -import org.junit.runners.model.Statement; - -/** - * {@link TestRule} implementation using {@link CloseableHttpClient} to check if Solr is running by sending - * {@literal GET} request to {@literal /admin/info/system}. - * - * @author Christoph Strobl - */ -public class RequiresSolrServer implements TestRule { - - private static final String PING_PATH = "/admin/info/system"; - - private final String baseUrl; - - private RequiresSolrServer(String baseUrl) { - this.baseUrl = baseUrl; - } - - public static RequiresSolrServer onLocalhost() { - return new RequiresSolrServer("http://localhost:8983/solr"); - } - - @Override - public Statement apply(Statement base, Description description) { - return new Statement() { - - @Override - public void evaluate() throws Throwable { - - checkServerRunning(); - base.evaluate(); - } - }; - } - - private void checkServerRunning() { - - try (CloseableHttpClient client = HttpClientBuilder.create().build()) { - CloseableHttpResponse response = client.execute(new HttpGet(baseUrl + PING_PATH)); - if (response != null && response.getStatusLine() != null) { - Assume.assumeThat(response.getStatusLine().getStatusCode(), Is.is(200)); - } - } catch (IOException e) { - throw new AssumptionViolatedException("SolrServer does not seem to be running", e); - } - } - -} diff --git a/solr/util/src/main/java/example/springdata/solr/test/util/SolrExampleContainer.java b/solr/util/src/main/java/example/springdata/solr/test/util/SolrExampleContainer.java new file mode 100644 index 000000000..859e05a25 --- /dev/null +++ b/solr/util/src/main/java/example/springdata/solr/test/util/SolrExampleContainer.java @@ -0,0 +1,79 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.solr.test.util; + +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.testcontainers.containers.GenericContainer; + +/** + * @author Jens Schauder + */ +public class SolrExampleContainer extends GenericContainer { + + private static final String IMAGE_NAME = "solr"; + private static final String DEFAULT_VERSION = "7"; + private static final int DEFAULT_PORT = 8983; + private final String example; + + public SolrExampleContainer(final String example) { + + super(IMAGE_NAME + ":" + DEFAULT_VERSION); + this.example = example; + this.withExposedPorts(DEFAULT_PORT) // + .withCommand("bash", "-c", "solr start -e " + example + " && tail -f /dev/null"); + + } + + @Override + public void start() { + super.start(); + + Integer port = getMappedPort(DEFAULT_PORT); + String url = "http://localhost:" + port + "/" + IMAGE_NAME; + + waitForSolr(url); + + } + + private void waitForSolr(String url) { + + long waitStart = System.currentTimeMillis(); + long maxWaitDuration = 10000; + + do { + + try (CloseableHttpClient client = HttpClientBuilder.create().build()) { + + CloseableHttpResponse response = client.execute(new HttpGet(url + "/" + example + "/admin/ping")); + if (response != null && response.getStatusLine() != null && response.getStatusLine().getStatusCode() == 200) { + + break; + } + + } catch (Exception e) { + e.printStackTrace(); + } + + } while (System.currentTimeMillis() < waitStart + maxWaitDuration); + } + + public String getSolrBaseUrl() { + return "http://localhost:" + getMappedPort(8983) + "/solr"; + } +} diff --git a/solr/util/src/main/java/example/springdata/solr/test/util/SolrInfrastructureRule.java b/solr/util/src/main/java/example/springdata/solr/test/util/SolrInfrastructureRule.java new file mode 100644 index 000000000..06fe99394 --- /dev/null +++ b/solr/util/src/main/java/example/springdata/solr/test/util/SolrInfrastructureRule.java @@ -0,0 +1,73 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.solr.test.util; + +import example.springdata.test.util.InfrastructureRule; + +import java.io.IOException; + +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.hamcrest.core.Is; +import org.junit.Assume; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.output.Slf4jLogConsumer; + +/** + * A JUnit Rule to locate or create a Solr instance including one of the sets of example data that is part of the Solr distribution. + * + * @author Jens Schauder + */ +public class SolrInfrastructureRule extends InfrastructureRule { + + private static final Logger LOG = LoggerFactory.getLogger(SolrInfrastructureRule.class); + + public SolrInfrastructureRule(String example) { + super(() -> checkNativeInstance("http://localhost:8983/solr", "/admin/info/system"), + () -> tryToStartInstanceInDocker(example)); + + } + + private static InfrastructureInfo checkNativeInstance(String baseUrl, String path) { + + try (CloseableHttpClient client = HttpClientBuilder.create().build()) { + + CloseableHttpResponse response = client.execute(new HttpGet(baseUrl + path)); + if (response != null && response.getStatusLine() != null) { + Assume.assumeThat(response.getStatusLine().getStatusCode(), Is.is(200)); + } + + return new InfrastructureInfo<>(true, baseUrl, null, () -> {}); + } catch (IOException e) { + return new InfrastructureInfo<>(false, null, e, () -> {}); + } + } + + private static InfrastructureInfo tryToStartInstanceInDocker(String example) { + + SolrExampleContainer solrContainer = // + new SolrExampleContainer(example) // + .withLogConsumer(new Slf4jLogConsumer(LOG)); + + solrContainer.start(); + + return new InfrastructureInfo<>(true, solrContainer.getSolrBaseUrl(), null, () -> solrContainer.stop()); + } + +} diff --git a/solr/util/src/test/java/example/springdata/solr/test/util/SolrExampleContainerTests.java b/solr/util/src/test/java/example/springdata/solr/test/util/SolrExampleContainerTests.java new file mode 100644 index 000000000..4ffef84dd --- /dev/null +++ b/solr/util/src/test/java/example/springdata/solr/test/util/SolrExampleContainerTests.java @@ -0,0 +1,17 @@ +package example.springdata.solr.test.util; + +import org.junit.Test ; + +/** + * @author Jens Schauder + */ +public class SolrExampleContainerTests { + + @Test + public void runsWithoutException() { + + SolrExampleContainer container = new SolrExampleContainer("techproducts"); + container.stop(); + } + +} diff --git a/util/pom.xml b/util/pom.xml new file mode 100644 index 000000000..658bfbec8 --- /dev/null +++ b/util/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + spring-data-examples-utils + jar + + + org.springframework.data.examples + spring-data-examples + 2.0.0.BUILD-SNAPSHOT + + + Spring Data Examples Test Utilities + Utilities for Spring Data Example tests + + + + org.springframework.data + spring-data-commons + + + junit + junit + + + org.assertj + assertj-core + test + + + + diff --git a/util/src/main/java/example/springdata/test/util/InfrastructureRule.java b/util/src/main/java/example/springdata/test/util/InfrastructureRule.java new file mode 100644 index 000000000..be98f33e5 --- /dev/null +++ b/util/src/main/java/example/springdata/test/util/InfrastructureRule.java @@ -0,0 +1,137 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.test.util; + +import static java.lang.Boolean.*; + +import lombok.Value; + +import java.util.function.Supplier; + +import org.junit.AssumptionViolatedException; +import org.junit.rules.ExternalResource; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.data.util.Lazy; +import org.springframework.util.Assert; + +/** + * A JUnit Rule for handling the discovery or creation of required infrastructure as for example databases. It supports + * multiple detection/creation strategies, supplied via {@link Supplier}. The system property + * {@code ignoreMissingInfrastructure} controls if tests get ignored when infrastructure is missing, or if the first + * test will throw an exception. + * + * This rule is intended to be instantiate by a Spring {@link org.springframework.context.ApplicationContext} and injected into tests. + * This enables shutdown of the infrastructure when the {@link org.springframework.context.ApplicationContext} gets shutdown. + * + * @author Jens Schauder + */ +public class InfrastructureRule extends ExternalResource implements DisposableBean { + + private static final String IGNORE_MISSING_INFRASTRUCTURE_PROPERTY_NAME = "ignoreMissingInfrastructure"; + private final boolean ignoreMissingInfrastructure; + private final Supplier> cachedResourceInfo; + private boolean isSubsequentCall = false; + + /** + * Creates or find the infrastructure using the provided {@link Supplier}s. The suppliers get tried until the first one returns a valid {@link InfrastructureInfo}. + * + * @param possibleSources different strategies to acquire the required infrastructure. + */ + public InfrastructureRule(Supplier>... possibleSources) { + + this(getIgnoreMissingInfrastructureProperty(), possibleSources); + } + + private static boolean getIgnoreMissingInfrastructureProperty() { + return Boolean.parseBoolean(System.getProperty(IGNORE_MISSING_INFRASTRUCTURE_PROPERTY_NAME, TRUE.toString())); + } + + InfrastructureRule(boolean ignoreMissingInfrastructure, Supplier>... possibleSources) { + + this(ignoreMissingInfrastructure, () -> buildInfoForFirstWorkingSource(possibleSources)); + + Assert.notEmpty(possibleSources, "We need at least one possibleSource for a resource."); + } + + private InfrastructureRule(boolean ignoreMissingInfrastructure, + Supplier> resourceInfoSupplier) { + + this.ignoreMissingInfrastructure = ignoreMissingInfrastructure; + this.cachedResourceInfo = Lazy.of(resourceInfoSupplier); + } + + @Override + protected void before() throws Throwable { + + if (!cachedResourceInfo.get().isValid()) { + String message = "Required infrastructure not available available"; + + if (ignoreMissingInfrastructure || isSubsequentCall) { + throw new AssumptionViolatedException(message); + } else { + isSubsequentCall = true; + throw new IllegalStateException(message); + } + } + } + + public T getInfo() { + return cachedResourceInfo.get().getInfo(); + } + + @Override + public void destroy() throws Exception { + cachedResourceInfo.get().destroy.run(); + } + + static private InfrastructureInfo buildInfoForFirstWorkingSource( + Supplier>... possibleSources) { + + Throwable lastException = null; + for (Supplier> source : possibleSources) { + + InfrastructureInfo info = source.get(); + if (info.isValid()) { + return info; + } + + lastException = info.getException(); + } + + return new InfrastructureInfo<>(false, null, lastException, () -> {}); + } + + /** + * Information about an attempt to find or create required infrastructure. + * + * @param the type of information offered to the tests about the infrastructure. + */ + @Value + public static class InfrastructureInfo { + + /** if the infrastructure was found or not */ + boolean valid; + + /** information about the infrastructure, e.g. an URL where to reach it. */ + T info; + + /** Any throwable thrown when failing to create or find the required infrastructure. */ + Throwable exception; + + /** A runnable to execute to shutdown the infrastructure. */ + Runnable destroy; + } +} diff --git a/util/src/test/java/example/springdata/test/util/InfrastructureRuleUnitTests.java b/util/src/test/java/example/springdata/test/util/InfrastructureRuleUnitTests.java new file mode 100644 index 000000000..b58e31e9e --- /dev/null +++ b/util/src/test/java/example/springdata/test/util/InfrastructureRuleUnitTests.java @@ -0,0 +1,67 @@ +package example.springdata.test.util; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.AssumptionViolatedException; +import org.junit.Test; +import org.junit.runners.model.Statement; + +/** + * Unit tests for the {@link InfrastructureRule}. + * + * @author Jens Schauder + */ +public class InfrastructureRuleUnitTests { + + static final Statement EMPTY_STATEMENT = new Statement() { + @Override + public void evaluate() throws Throwable { + + } + }; + + @Test + public void failingRuleIgnoresTestByDefault() { + + InfrastructureRule rule = new InfrastructureRule<>(true, + () -> new InfrastructureRule.InfrastructureInfo<>(false, null, null, () -> {})); + + Statement statement = rule.apply(EMPTY_STATEMENT, null); + + assertThatExceptionOfType(AssumptionViolatedException.class).isThrownBy(statement::evaluate); + } + + @Test + public void succeedingRuleExecutesTest() throws Throwable { + + InfrastructureRule rule = new InfrastructureRule<>(true, + () -> new InfrastructureRule.InfrastructureInfo<>(true, null, null, () -> {})); + + Statement originalStatement = mock(Statement.class); + + rule.apply(originalStatement, null).evaluate(); + + verify(originalStatement, times(1)).evaluate(); + } + + @Test + public void ifRequiredTheFirstTestWithMissingInfrastructureFails() { + + InfrastructureRule rule = new InfrastructureRule<>(false, + () -> new InfrastructureRule.InfrastructureInfo<>(false, null, null, () -> {})); + + Statement statement = rule.apply(EMPTY_STATEMENT, null); + + assertThatIllegalStateException().isThrownBy(statement::evaluate); + assertThatExceptionOfType(AssumptionViolatedException.class).isThrownBy(statement::evaluate); + + } + + @Test + public void infrastructureRuleRequiresAtLeastOneSupplier() { + + assertThatIllegalArgumentException().isThrownBy(InfrastructureRule::new); + } + +}