From 98515fc93fbfc6292bf224d0640fe7fe61e6041e Mon Sep 17 00:00:00 2001 From: Sergei Egorov Date: Thu, 9 Apr 2020 14:16:48 +0200 Subject: [PATCH 01/15] Add R2DBC URL support --- modules/postgresql/build.gradle | 4 + .../PostgreSQLR2DBCDatabaseContainer.java | 33 ++++ ...tgreSQLR2DBCDatabaseContainerProvider.java | 26 +++ ...iners.r2dbc.R2DBCDatabaseContainerProvider | 1 + .../PostgreSQLR2DBCDatabaseContainerTest.java | 42 +++++ .../src/test/resources/logback-test.xml | 16 ++ modules/r2dbc/build.gradle | 10 ++ .../r2dbc/CancellableSubscription.java | 23 +++ .../r2dbc/ConnectionPublisher.java | 152 ++++++++++++++++++ .../r2dbc/EmptySubscription.java | 17 ++ .../java/org/testcontainers/r2dbc/Hidden.java | 70 ++++++++ .../r2dbc/R2DBCDatabaseContainer.java | 9 ++ .../r2dbc/R2DBCDatabaseContainerProvider.java | 15 ++ .../TestcontainersR2DBCConnectionFactory.java | 104 ++++++++++++ .../io.r2dbc.spi.ConnectionFactoryProvider | 1 + ...tcontainersR2DBCConnectionFactoryTest.java | 79 +++++++++ .../r2dbc/src/test/resources/logback-test.xml | 16 ++ 17 files changed, 618 insertions(+) create mode 100644 modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainer.java create mode 100644 modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerProvider.java create mode 100644 modules/postgresql/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider create mode 100644 modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerTest.java create mode 100644 modules/postgresql/src/test/resources/logback-test.xml create mode 100644 modules/r2dbc/build.gradle create mode 100644 modules/r2dbc/src/main/java/org/testcontainers/r2dbc/CancellableSubscription.java create mode 100644 modules/r2dbc/src/main/java/org/testcontainers/r2dbc/ConnectionPublisher.java create mode 100644 modules/r2dbc/src/main/java/org/testcontainers/r2dbc/EmptySubscription.java create mode 100644 modules/r2dbc/src/main/java/org/testcontainers/r2dbc/Hidden.java create mode 100644 modules/r2dbc/src/main/java/org/testcontainers/r2dbc/R2DBCDatabaseContainer.java create mode 100644 modules/r2dbc/src/main/java/org/testcontainers/r2dbc/R2DBCDatabaseContainerProvider.java create mode 100644 modules/r2dbc/src/main/java/org/testcontainers/r2dbc/TestcontainersR2DBCConnectionFactory.java create mode 100644 modules/r2dbc/src/main/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider create mode 100644 modules/r2dbc/src/test/java/org/testcontainers/r2dbc/TestcontainersR2DBCConnectionFactoryTest.java create mode 100644 modules/r2dbc/src/test/resources/logback-test.xml diff --git a/modules/postgresql/build.gradle b/modules/postgresql/build.gradle index 9acf9ead2aa..8e26d9a47fa 100644 --- a/modules/postgresql/build.gradle +++ b/modules/postgresql/build.gradle @@ -2,8 +2,12 @@ description = "Testcontainers :: JDBC :: PostgreSQL" dependencies { compile project(':jdbc') + compileOnly project(':r2dbc') testCompile 'org.postgresql:postgresql:42.2.10' testCompile 'commons-dbutils:commons-dbutils:1.7' testCompile 'com.zaxxer:HikariCP-java6:2.3.13' + + testCompile project(':r2dbc') + testCompile 'io.r2dbc:r2dbc-postgresql:0.8.1.RELEASE' } diff --git a/modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainer.java b/modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainer.java new file mode 100644 index 00000000000..9bfb9fdee05 --- /dev/null +++ b/modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainer.java @@ -0,0 +1,33 @@ +package org.testcontainers.containers; + +import io.r2dbc.spi.ConnectionFactoryOptions; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Delegate; +import org.testcontainers.lifecycle.Startable; +import org.testcontainers.r2dbc.R2DBCDatabaseContainer; + +@RequiredArgsConstructor +public final class PostgreSQLR2DBCDatabaseContainer implements R2DBCDatabaseContainer { + + @Delegate(types = Startable.class) + private final PostgreSQLContainer container; + + public static ConnectionFactoryOptions getOptions(PostgreSQLContainer container) { + ConnectionFactoryOptions options = ConnectionFactoryOptions.builder() + .option(ConnectionFactoryOptions.DRIVER, PostgreSQLR2DBCDatabaseContainerProvider.DRIVER) + .build(); + + return new PostgreSQLR2DBCDatabaseContainer(container).configure(options); + } + + @Override + public ConnectionFactoryOptions configure(ConnectionFactoryOptions options) { + return options.mutate() + .option(ConnectionFactoryOptions.HOST, container.getContainerIpAddress()) + .option(ConnectionFactoryOptions.PORT, container.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT)) + .option(ConnectionFactoryOptions.DATABASE, container.getDatabaseName()) + .option(ConnectionFactoryOptions.USER, container.getUsername()) + .option(ConnectionFactoryOptions.PASSWORD, container.getPassword()) + .build(); + } +} diff --git a/modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerProvider.java b/modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerProvider.java new file mode 100644 index 00000000000..7bec3e35a56 --- /dev/null +++ b/modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerProvider.java @@ -0,0 +1,26 @@ +package org.testcontainers.containers; + +import io.r2dbc.spi.ConnectionFactoryOptions; +import org.testcontainers.r2dbc.R2DBCDatabaseContainer; +import org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider; + +public final class PostgreSQLR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider { + + static final String DRIVER = "postgresql"; + + @Override + public boolean supports(ConnectionFactoryOptions options) { + return DRIVER.equals(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)); + } + + @Override + public R2DBCDatabaseContainer createContainer(ConnectionFactoryOptions options) { + PostgreSQLContainer container = new PostgreSQLContainer<>(options.getRequiredValue(IMAGE_OPTION)) + .withDatabaseName(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)); + + if (Boolean.TRUE.equals(options.getValue(REUSABLE_OPTION))) { + container.withReuse(true); + } + return new PostgreSQLR2DBCDatabaseContainer(container); + } +} diff --git a/modules/postgresql/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider b/modules/postgresql/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider new file mode 100644 index 00000000000..6224c0e6093 --- /dev/null +++ b/modules/postgresql/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider @@ -0,0 +1 @@ +org.testcontainers.containers.PostgreSQLR2DBCDatabaseContainerProvider diff --git a/modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerTest.java b/modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerTest.java new file mode 100644 index 00000000000..c5e9b36813c --- /dev/null +++ b/modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerTest.java @@ -0,0 +1,42 @@ +package org.testcontainers.containers; + +import io.r2dbc.spi.Closeable; +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactories; +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import static org.junit.Assert.*; + +public class PostgreSQLR2DBCDatabaseContainerTest { + + @Test + public void testGetOptions() { + try (PostgreSQLContainer container = new PostgreSQLContainer<>()) { + container.start(); + + int result = Flux + .usingWhen( + Mono.just( + ConnectionFactories.get( + PostgreSQLR2DBCDatabaseContainer.getOptions(container) + ) + ), + connectionFactory -> { + return Flux + .usingWhen( + connectionFactory.create(), + connection -> connection.createStatement("SELECT 42").execute(), + Connection::close + ) + .flatMap(it -> it.map((row, meta) -> (Integer) row.get(0))); + }, + it -> ((Closeable) it).close() + ) + .blockFirst(); + + assertEquals(42, result); + } + } +} diff --git a/modules/postgresql/src/test/resources/logback-test.xml b/modules/postgresql/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..535e406fc13 --- /dev/null +++ b/modules/postgresql/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + diff --git a/modules/r2dbc/build.gradle b/modules/r2dbc/build.gradle new file mode 100644 index 00000000000..0914e951ce2 --- /dev/null +++ b/modules/r2dbc/build.gradle @@ -0,0 +1,10 @@ +description = "Testcontainers :: R2DBC" + +dependencies { + compile project(':testcontainers') + compile 'io.r2dbc:r2dbc-spi:0.8.1.RELEASE' + + testCompile 'org.assertj:assertj-core:3.14.0' + testCompile 'io.r2dbc:r2dbc-postgresql:0.8.1.RELEASE' + testCompile project(':postgresql') +} diff --git a/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/CancellableSubscription.java b/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/CancellableSubscription.java new file mode 100644 index 00000000000..e64d19a9f49 --- /dev/null +++ b/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/CancellableSubscription.java @@ -0,0 +1,23 @@ +package org.testcontainers.r2dbc; + +import org.reactivestreams.Subscription; + +import java.util.concurrent.atomic.AtomicBoolean; + +class CancellableSubscription implements Subscription { + + private final AtomicBoolean cancelled = new AtomicBoolean(); + + @Override + public void request(long n) { + } + + @Override + public void cancel() { + cancelled.set(true); + } + + public boolean isCancelled() { + return cancelled.get(); + } +} diff --git a/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/ConnectionPublisher.java b/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/ConnectionPublisher.java new file mode 100644 index 00000000000..d4dde8a9de7 --- /dev/null +++ b/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/ConnectionPublisher.java @@ -0,0 +1,152 @@ +package org.testcontainers.r2dbc; + +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactory; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +class ConnectionPublisher implements Publisher { + + private final Supplier> futureSupplier; + + ConnectionPublisher(Supplier> futureSupplier) { + this.futureSupplier = futureSupplier; + } + + @Override + public void subscribe(Subscriber actual) { + actual.onSubscribe(new StateMachineSubscription(actual)); + } + + /** + * Design notes: + * - ConnectionPublisher is Mono-like (0..1), the request amount is ignored + * - given the testing nature, the performance requirements are less strict + * - "synchronized" is used to avoid races + * - Reactive Streams spec violations are not checked (e.g. non-positive request) + */ + private class StateMachineSubscription implements Subscription { + + private final Subscriber actual; + + Subscription subscriptionState; + + StateMachineSubscription(Subscriber actual) { + this.actual = actual; + subscriptionState = new WaitRequestSubscriptionState(); + } + + @Override + public synchronized void request(long n) { + subscriptionState.request(n); + } + + @Override + public synchronized void cancel() { + subscriptionState.cancel(); + } + + synchronized void transitionTo(SubscriptionState newState) { + subscriptionState = newState; + newState.enter(); + } + + abstract class SubscriptionState implements Subscription { + void enter() { + } + } + + class WaitRequestSubscriptionState extends SubscriptionState { + + @Override + public void request(long n) { + transitionTo(new WaitFutureCompletionSubscriptionState()); + } + + @Override + public void cancel() { + } + } + + class WaitFutureCompletionSubscriptionState extends SubscriptionState { + + private CompletableFuture future; + + @Override + void enter() { + this.future = futureSupplier.get(); + + future.whenComplete((connectionFactory, e) -> { + if (e != null) { + actual.onSubscribe(EmptySubscription.INSTANCE); + actual.onError(e); + return; + } + + Publisher publisher = connectionFactory.create(); + transitionTo(new ProxySubscriptionState(publisher)); + }); + } + + @Override + public void request(long n) { + } + + @Override + public void cancel() { + future.cancel(true); + } + } + + class ProxySubscriptionState extends SubscriptionState implements Subscriber { + + private final Publisher publisher; + + private Subscription s; + + ProxySubscriptionState(Publisher publisher) { + this.publisher = publisher; + } + + @Override + void enter() { + publisher.subscribe(this); + } + + @Override + public void request(long n) { + // Ignore + } + + @Override + public void cancel() { + s.cancel(); + } + + @Override + public void onSubscribe(Subscription s) { + this.s = s; + s.request(1); + } + + @Override + public void onNext(Connection connection) { + actual.onNext(connection); + } + + @Override + public void onError(Throwable t) { + actual.onError(t); + } + + @Override + public void onComplete() { + actual.onComplete(); + } + } + } +} diff --git a/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/EmptySubscription.java b/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/EmptySubscription.java new file mode 100644 index 00000000000..7828d121c6f --- /dev/null +++ b/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/EmptySubscription.java @@ -0,0 +1,17 @@ +package org.testcontainers.r2dbc; + +import org.reactivestreams.Subscription; + +enum EmptySubscription implements Subscription { + INSTANCE; + + @Override + public void request(long n) { + + } + + @Override + public void cancel() { + + } +} diff --git a/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/Hidden.java b/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/Hidden.java new file mode 100644 index 00000000000..78d5e7b9b35 --- /dev/null +++ b/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/Hidden.java @@ -0,0 +1,70 @@ +package org.testcontainers.r2dbc; + +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryOptions; +import io.r2dbc.spi.ConnectionFactoryProvider; + +/** + * Hide inner classes that must be public due to the way {@link java.util.ServiceLoader} works + */ +class Hidden { + public static final class TestcontainersR2DBCConnectionFactoryProvider implements ConnectionFactoryProvider { + + public static final String DRIVER = "tc"; + + @Override + public ConnectionFactory create(ConnectionFactoryOptions options) { + options = sanitize(options); + options = removeProxying(options); + + return new TestcontainersR2DBCConnectionFactory(options); + } + + private ConnectionFactoryOptions sanitize(ConnectionFactoryOptions options) { + ConnectionFactoryOptions.Builder builder = options.mutate(); + + Object reusable = options.getValue(R2DBCDatabaseContainerProvider.REUSABLE_OPTION); + if (reusable instanceof String) { + builder.option(R2DBCDatabaseContainerProvider.REUSABLE_OPTION, Boolean.valueOf((String) reusable)); + } + return builder.build(); + } + + private ConnectionFactoryOptions removeProxying(ConnectionFactoryOptions options) { + // To delegate to the next factory provider, inspect the PROTOCOL and convert it to the next DRIVER and PROTOCOL values. + // + // example: + // | Property | Input | Output | + // |----------|-----------------|--------------| + // | DRIVER | tc | postgres | + // | PROTOCOL | postgres | | + + String protocol = options.getRequiredValue(ConnectionFactoryOptions.PROTOCOL); + if (protocol.trim().length() == 0) { + throw new IllegalArgumentException("Invalid protocol: " + protocol); + } + String[] protocols = protocol.split(":", 2); + String driverDelegate = protocols[0]; + + // when protocol does NOT contain COLON, the length becomes 1 + String protocolDelegate = protocols.length == 2 ? protocols[1] : ""; + + return ConnectionFactoryOptions.builder() + .from(options) + .option(ConnectionFactoryOptions.DRIVER, driverDelegate) + .option(ConnectionFactoryOptions.PROTOCOL, protocolDelegate) + .build(); + } + + @Override + public boolean supports(ConnectionFactoryOptions options) { + return DRIVER.equals(options.getValue(ConnectionFactoryOptions.DRIVER)); + } + + @Override + public String getDriver() { + return DRIVER; + } + + } +} diff --git a/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/R2DBCDatabaseContainer.java b/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/R2DBCDatabaseContainer.java new file mode 100644 index 00000000000..a303cbafff8 --- /dev/null +++ b/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/R2DBCDatabaseContainer.java @@ -0,0 +1,9 @@ +package org.testcontainers.r2dbc; + +import io.r2dbc.spi.ConnectionFactoryOptions; +import org.testcontainers.lifecycle.Startable; + +public interface R2DBCDatabaseContainer extends Startable { + + ConnectionFactoryOptions configure(ConnectionFactoryOptions options); +} diff --git a/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/R2DBCDatabaseContainerProvider.java b/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/R2DBCDatabaseContainerProvider.java new file mode 100644 index 00000000000..dab7aee3cf2 --- /dev/null +++ b/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/R2DBCDatabaseContainerProvider.java @@ -0,0 +1,15 @@ +package org.testcontainers.r2dbc; + +import io.r2dbc.spi.ConnectionFactoryOptions; +import io.r2dbc.spi.Option; + +public interface R2DBCDatabaseContainerProvider { + + Option REUSABLE_OPTION = Option.valueOf("TC_REUSABLE"); + + Option IMAGE_OPTION = Option.valueOf("TC_IMAGE"); + + boolean supports(ConnectionFactoryOptions options); + + R2DBCDatabaseContainer createContainer(ConnectionFactoryOptions options); +} diff --git a/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/TestcontainersR2DBCConnectionFactory.java b/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/TestcontainersR2DBCConnectionFactory.java new file mode 100644 index 00000000000..1a0fc57ad8c --- /dev/null +++ b/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/TestcontainersR2DBCConnectionFactory.java @@ -0,0 +1,104 @@ +package org.testcontainers.r2dbc; + +import io.r2dbc.spi.Closeable; +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactories; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryMetadata; +import io.r2dbc.spi.ConnectionFactoryOptions; +import org.reactivestreams.Publisher; +import org.testcontainers.lifecycle.Startable; + +import java.util.ServiceLoader; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.StreamSupport; + +class TestcontainersR2DBCConnectionFactory implements ConnectionFactory, Closeable { + + private static final AtomicLong THREAD_COUNT = new AtomicLong(); + + private static final Executor EXECUTOR = Executors.newCachedThreadPool(r -> { + Thread thread = new Thread(r); + thread.setName("testcontainers-r2dbc-" + THREAD_COUNT.getAndIncrement()); + thread.setDaemon(true); + return thread; + }); + + private final ConnectionFactoryOptions options; + + private final R2DBCDatabaseContainerProvider containerProvider; + + private CompletableFuture future; + + TestcontainersR2DBCConnectionFactory(ConnectionFactoryOptions options) { + this.options = options; + + containerProvider = StreamSupport.stream(ServiceLoader.load(R2DBCDatabaseContainerProvider.class).spliterator(), false) + .filter(it -> it.supports(options)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("Missing provider for " + options)); + } + + @Override + public Publisher create() { + return new ConnectionPublisher( + () -> { + if (future == null) { + synchronized (this) { + if (future == null) { + future = CompletableFuture.supplyAsync(() -> { + R2DBCDatabaseContainer container = containerProvider.createContainer(options); + container.start(); + return container; + }, EXECUTOR); + } + } + } + return future.thenApply(it -> { + return ConnectionFactories.find( + it.configure(options) + ); + }); + } + ); + } + + @Override + public ConnectionFactoryMetadata getMetadata() { + // TODO check whether it is fine to call it without setting the host/port and other values + return ConnectionFactories.find(options).getMetadata(); + } + + @Override + public Publisher close() { + return s -> { + CompletableFuture futureRef; + synchronized (this) { + futureRef = this.future; + this.future = null; + } + + CancellableSubscription subscription = new CancellableSubscription(); + s.onSubscribe(subscription); + + if (futureRef == null) { + if (!subscription.isCancelled()) { + s.onComplete(); + } + } else { + futureRef.thenAcceptAsync(Startable::stop, EXECUTOR); + + EXECUTOR.execute(() -> { + futureRef.cancel(true); + if (!subscription.isCancelled()) { + s.onComplete(); + } + }); + } + }; + } + +} diff --git a/modules/r2dbc/src/main/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider b/modules/r2dbc/src/main/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider new file mode 100644 index 00000000000..23f1702c57a --- /dev/null +++ b/modules/r2dbc/src/main/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider @@ -0,0 +1 @@ +org.testcontainers.r2dbc.Hidden$TestcontainersR2DBCConnectionFactoryProvider diff --git a/modules/r2dbc/src/test/java/org/testcontainers/r2dbc/TestcontainersR2DBCConnectionFactoryTest.java b/modules/r2dbc/src/test/java/org/testcontainers/r2dbc/TestcontainersR2DBCConnectionFactoryTest.java new file mode 100644 index 00000000000..b75f465336e --- /dev/null +++ b/modules/r2dbc/src/test/java/org/testcontainers/r2dbc/TestcontainersR2DBCConnectionFactoryTest.java @@ -0,0 +1,79 @@ +package org.testcontainers.r2dbc; + +import io.r2dbc.postgresql.api.PostgresqlException; +import io.r2dbc.spi.Closeable; +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactories; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.Result; +import org.assertj.core.api.Assertions; +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestcontainersR2DBCConnectionFactoryTest { + + @Test + public void failsOnUnknownProvider() { + String nonExistingProvider = UUID.randomUUID().toString(); + Assertions + .assertThatThrownBy(() -> { + ConnectionFactories.get(String.format("r2dbc:tc:%s:///db", nonExistingProvider)); + }) + .hasMessageContaining("Missing provider") + .hasMessageContaining(nonExistingProvider); + } + + @Test + public void reusesUntilConnectionFactoryIsClosed() { + String url = "r2dbc:tc:postgresql:///db?TC_IMAGE=postgres:10-alpine"; + ConnectionFactory connectionFactory = ConnectionFactories.get(url); + + Integer updated = Flux + .usingWhen( + connectionFactory.create(), + connection -> { + return Mono + .from(connection.createStatement("CREATE TABLE test(id integer PRIMARY KEY)").execute()) + .thenMany(connection.createStatement("INSERT INTO test(id) VALUES(123)").execute()) + .flatMap(Result::getRowsUpdated); + }, + Connection::close + ) + .blockFirst(); + + assertThat(updated).isEqualTo(1); + + Flux select = Flux + .usingWhen( + Flux.defer(connectionFactory::create), + connection -> { + return Flux + .from(connection.createStatement("SELECT COUNT(*) FROM test").execute()) + .flatMap(it -> it.map((row, meta) -> (Long) row.get(0))); + }, + Connection::close + ); + + Long rows = select.blockFirst(); + + assertThat(rows).isEqualTo(1); + + close(connectionFactory); + + Assertions + .assertThatThrownBy(select::blockFirst) + .isInstanceOf(PostgresqlException.class) + // relation "X" does not exists + // https://github.com/postgres/postgres/blob/REL_10_0/src/backend/utils/errcodes.txt#L349 + .returns("42P01", e -> ((PostgresqlException) e).getErrorDetails().getCode()); + } + + private static void close(ConnectionFactory connectionFactory) { + Mono.from(((Closeable) connectionFactory).close()).block(); + } +} diff --git a/modules/r2dbc/src/test/resources/logback-test.xml b/modules/r2dbc/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..535e406fc13 --- /dev/null +++ b/modules/r2dbc/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + From d3409df8e09c28f0f3531d9f2918e55ebd37eebb Mon Sep 17 00:00:00 2001 From: Sergei Egorov Date: Thu, 9 Apr 2020 15:00:24 +0200 Subject: [PATCH 02/15] fix a race --- .../r2dbc/ConnectionPublisher.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/ConnectionPublisher.java b/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/ConnectionPublisher.java index d4dde8a9de7..8c0682498e6 100644 --- a/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/ConnectionPublisher.java +++ b/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/ConnectionPublisher.java @@ -108,6 +108,8 @@ class ProxySubscriptionState extends SubscriptionState implements Subscriber publisher) { this.publisher = publisher; } @@ -123,14 +125,21 @@ public void request(long n) { } @Override - public void cancel() { - s.cancel(); + public synchronized void cancel() { + cancelled = true; + if (s != null) { + s.cancel(); + } } @Override - public void onSubscribe(Subscription s) { + public synchronized void onSubscribe(Subscription s) { this.s = s; - s.request(1); + if (!cancelled) { + s.request(1); + } else { + s.cancel(); + } } @Override From 3e2b34b15aa06c385a793f1b50e076a32c4fb8d6 Mon Sep 17 00:00:00 2001 From: Sergei Egorov Date: Thu, 9 Apr 2020 15:14:01 +0200 Subject: [PATCH 03/15] move javadoc to the top class --- .../testcontainers/r2dbc/ConnectionPublisher.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/ConnectionPublisher.java b/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/ConnectionPublisher.java index 8c0682498e6..489d545429d 100644 --- a/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/ConnectionPublisher.java +++ b/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/ConnectionPublisher.java @@ -9,6 +9,13 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; +/** + * Design notes: + * - ConnectionPublisher is Mono-like (0..1), the request amount is ignored + * - given the testing nature, the performance requirements are less strict + * - "synchronized" is used to avoid races + * - Reactive Streams spec violations are not checked (e.g. non-positive request) + */ class ConnectionPublisher implements Publisher { private final Supplier> futureSupplier; @@ -22,13 +29,6 @@ public void subscribe(Subscriber actual) { actual.onSubscribe(new StateMachineSubscription(actual)); } - /** - * Design notes: - * - ConnectionPublisher is Mono-like (0..1), the request amount is ignored - * - given the testing nature, the performance requirements are less strict - * - "synchronized" is used to avoid races - * - Reactive Streams spec violations are not checked (e.g. non-positive request) - */ private class StateMachineSubscription implements Subscription { private final Subscriber actual; From 84ce94d515d83704969dae75e502f27228c6ac04 Mon Sep 17 00:00:00 2001 From: Sergei Egorov Date: Thu, 9 Apr 2020 16:30:34 +0200 Subject: [PATCH 04/15] use `auto-service`, add `testUrlSupport` test --- modules/postgresql/build.gradle | 4 +++ ...tgreSQLR2DBCDatabaseContainerProvider.java | 5 +++- ...iners.r2dbc.R2DBCDatabaseContainerProvider | 1 - .../PostgreSQLR2DBCDatabaseContainerTest.java | 26 +++++++++++++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) delete mode 100644 modules/postgresql/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider diff --git a/modules/postgresql/build.gradle b/modules/postgresql/build.gradle index 8e26d9a47fa..3e828fe0d8e 100644 --- a/modules/postgresql/build.gradle +++ b/modules/postgresql/build.gradle @@ -1,8 +1,12 @@ description = "Testcontainers :: JDBC :: PostgreSQL" dependencies { + annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6' + compileOnly 'com.google.auto.service:auto-service:1.0-rc6' + compile project(':jdbc') compileOnly project(':r2dbc') + compileOnly 'io.r2dbc:r2dbc-postgresql:0.8.1.RELEASE' testCompile 'org.postgresql:postgresql:42.2.10' testCompile 'commons-dbutils:commons-dbutils:1.7' diff --git a/modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerProvider.java b/modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerProvider.java index 7bec3e35a56..d716e334403 100644 --- a/modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerProvider.java +++ b/modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerProvider.java @@ -1,12 +1,15 @@ package org.testcontainers.containers; +import com.google.auto.service.AutoService; +import io.r2dbc.postgresql.PostgresqlConnectionFactoryProvider; import io.r2dbc.spi.ConnectionFactoryOptions; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; import org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider; +@AutoService(R2DBCDatabaseContainerProvider.class) public final class PostgreSQLR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider { - static final String DRIVER = "postgresql"; + static final String DRIVER = PostgresqlConnectionFactoryProvider.POSTGRESQL_DRIVER; @Override public boolean supports(ConnectionFactoryOptions options) { diff --git a/modules/postgresql/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider b/modules/postgresql/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider deleted file mode 100644 index 6224c0e6093..00000000000 --- a/modules/postgresql/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider +++ /dev/null @@ -1 +0,0 @@ -org.testcontainers.containers.PostgreSQLR2DBCDatabaseContainerProvider diff --git a/modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerTest.java b/modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerTest.java index c5e9b36813c..8e937b405d7 100644 --- a/modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerTest.java +++ b/modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerTest.java @@ -3,6 +3,8 @@ import io.r2dbc.spi.Closeable; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactories; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.Result; import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -39,4 +41,28 @@ public void testGetOptions() { assertEquals(42, result); } } + + @Test + public void testUrlSupport() { + String url = "r2dbc:tc:postgresql:///db?TC_IMAGE=postgres:10-alpine"; + ConnectionFactory connectionFactory = ConnectionFactories.get(url); + try { + int updated = Flux + .usingWhen( + connectionFactory.create(), + connection -> { + return Mono + .from(connection.createStatement("CREATE TABLE test(id integer PRIMARY KEY)").execute()) + .thenMany(connection.createStatement("INSERT INTO test(id) VALUES(123)").execute()) + .flatMap(Result::getRowsUpdated); + }, + Connection::close + ) + .blockFirst(); + + assertEquals(updated, 1); + } finally { + Mono.from(((Closeable) connectionFactory).close()).block(); + } + } } From 7b4a8c668aa6e6ef881fe64a8c4cd1d9b2380366 Mon Sep 17 00:00:00 2001 From: Sergei Egorov Date: Thu, 9 Apr 2020 16:31:30 +0200 Subject: [PATCH 05/15] Add MSSQL R2DBC support --- modules/mssqlserver/build.gradle | 12 ++++ .../MSSQLR2DBCDatabaseContainer.java | 34 ++++++++++ .../MSSQLR2DBCDatabaseContainerProvider.java | 28 +++++++++ .../MSSQLR2DBCDatabaseContainerTest.java | 62 +++++++++++++++++++ .../container-license-acceptance.txt | 1 + .../src/test/resources/logback-test.xml | 16 +++++ 6 files changed, 153 insertions(+) create mode 100644 modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainer.java create mode 100644 modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerProvider.java create mode 100644 modules/mssqlserver/src/test/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerTest.java create mode 100644 modules/mssqlserver/src/test/resources/container-license-acceptance.txt create mode 100644 modules/mssqlserver/src/test/resources/logback-test.xml diff --git a/modules/mssqlserver/build.gradle b/modules/mssqlserver/build.gradle index 62434722d63..19de3eca1a7 100644 --- a/modules/mssqlserver/build.gradle +++ b/modules/mssqlserver/build.gradle @@ -1,5 +1,17 @@ description = "Testcontainers :: MS SQL Server" dependencies { + annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6' + compileOnly 'com.google.auto.service:auto-service:1.0-rc6' + compile project(':jdbc') + + compileOnly project(':r2dbc') + compileOnly 'io.r2dbc:r2dbc-mssql:0.8.1.RELEASE' + + testCompile project(':r2dbc') + testCompile 'io.r2dbc:r2dbc-mssql:0.8.1.RELEASE' + + // MSSQL's wait strategy requires the JDBC driver + testCompile 'com.microsoft.sqlserver:mssql-jdbc:6.1.0.jre8' } diff --git a/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainer.java b/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainer.java new file mode 100644 index 00000000000..27c486c218c --- /dev/null +++ b/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainer.java @@ -0,0 +1,34 @@ +package org.testcontainers.containers; + +import io.r2dbc.spi.ConnectionFactoryOptions; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Delegate; +import org.testcontainers.lifecycle.Startable; +import org.testcontainers.r2dbc.R2DBCDatabaseContainer; + +@RequiredArgsConstructor +public class MSSQLR2DBCDatabaseContainer implements R2DBCDatabaseContainer { + + @Delegate(types = Startable.class) + private final MSSQLServerContainer container; + + public static ConnectionFactoryOptions getOptions(MSSQLServerContainer container) { + ConnectionFactoryOptions options = ConnectionFactoryOptions.builder() + .option(ConnectionFactoryOptions.DRIVER, MSSQLR2DBCDatabaseContainerProvider.DRIVER) + .build(); + + return new MSSQLR2DBCDatabaseContainer(container).configure(options); + } + + @Override + public ConnectionFactoryOptions configure(ConnectionFactoryOptions options) { + return options.mutate() + .option(ConnectionFactoryOptions.HOST, container.getContainerIpAddress()) + .option(ConnectionFactoryOptions.PORT, container.getMappedPort(MSSQLServerContainer.MS_SQL_SERVER_PORT)) + // TODO enable if/when MSSQLServerContainer adds support for customizing the DB name + // .option(ConnectionFactoryOptions.DATABASE, container.getDatabasseName()) + .option(ConnectionFactoryOptions.USER, container.getUsername()) + .option(ConnectionFactoryOptions.PASSWORD, container.getPassword()) + .build(); + } +} diff --git a/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerProvider.java b/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerProvider.java new file mode 100644 index 00000000000..dd03be633c6 --- /dev/null +++ b/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerProvider.java @@ -0,0 +1,28 @@ +package org.testcontainers.containers; + +import com.google.auto.service.AutoService; +import io.r2dbc.mssql.MssqlConnectionFactoryProvider; +import io.r2dbc.spi.ConnectionFactoryOptions; +import org.testcontainers.r2dbc.R2DBCDatabaseContainer; +import org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider; + +@AutoService(R2DBCDatabaseContainerProvider.class) +public class MSSQLR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider { + + static final String DRIVER = MssqlConnectionFactoryProvider.MSSQL_DRIVER; + + @Override + public boolean supports(ConnectionFactoryOptions options) { + return DRIVER.equals(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)); + } + + @Override + public R2DBCDatabaseContainer createContainer(ConnectionFactoryOptions options) { + MSSQLServerContainer container = new MSSQLServerContainer<>(options.getRequiredValue(IMAGE_OPTION)); + + if (Boolean.TRUE.equals(options.getValue(REUSABLE_OPTION))) { + container.withReuse(true); + } + return new MSSQLR2DBCDatabaseContainer(container); + } +} diff --git a/modules/mssqlserver/src/test/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerTest.java b/modules/mssqlserver/src/test/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerTest.java new file mode 100644 index 00000000000..922176aaeb7 --- /dev/null +++ b/modules/mssqlserver/src/test/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerTest.java @@ -0,0 +1,62 @@ +package org.testcontainers.containers; + +import io.r2dbc.spi.Closeable; +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactories; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.Result; +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import static org.junit.Assert.*; + +public class MSSQLR2DBCDatabaseContainerTest { + + @Test + public void testGetOptions() { + try (MSSQLServerContainer container = new MSSQLServerContainer<>()) { + container.start(); + + ConnectionFactory connectionFactory = ConnectionFactories.get( + MSSQLR2DBCDatabaseContainer.getOptions(container) + ); + + int result = Flux + .usingWhen( + connectionFactory.create(), + connection -> connection.createStatement("SELECT 42").execute(), + Connection::close + ) + .flatMap(it -> it.map((row, meta) -> (Integer) row.get(0))) + .blockFirst(); + + assertEquals(42, result); + } + } + + @Test + public void testUrlSupport() { + String url = "r2dbc:tc:sqlserver:///?TC_IMAGE=mcr.microsoft.com%2Fmssql%2Fserver%3A2017-CU12"; + ConnectionFactory connectionFactory = ConnectionFactories.get(url); + try { + int updated = Flux + .usingWhen( + connectionFactory.create(), + connection -> { + return Mono + .from(connection.createStatement("CREATE DATABASE [test];").execute()) + .thenMany(connection.createStatement("CREATE TABLE test(id integer PRIMARY KEY)").execute()) + .thenMany(connection.createStatement("INSERT INTO test(id) VALUES(123)").execute()) + .flatMap(Result::getRowsUpdated); + }, + Connection::close + ) + .blockFirst(); + + assertEquals(updated, 1); + } finally { + Mono.from(((Closeable) connectionFactory).close()).block(); + } + } +} diff --git a/modules/mssqlserver/src/test/resources/container-license-acceptance.txt b/modules/mssqlserver/src/test/resources/container-license-acceptance.txt new file mode 100644 index 00000000000..8a704b9c066 --- /dev/null +++ b/modules/mssqlserver/src/test/resources/container-license-acceptance.txt @@ -0,0 +1 @@ +mcr.microsoft.com/mssql/server:2017-CU12 diff --git a/modules/mssqlserver/src/test/resources/logback-test.xml b/modules/mssqlserver/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..535e406fc13 --- /dev/null +++ b/modules/mssqlserver/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + From 8903ed8523dc21c5ce2decb53917baba6273508f Mon Sep 17 00:00:00 2001 From: Sergei Egorov Date: Thu, 9 Apr 2020 16:51:29 +0200 Subject: [PATCH 06/15] Add MySQL --- modules/mysql/build.gradle | 9 +++ .../MySQLR2DBCDatabaseContainer.java | 33 +++++++++ .../MySQLR2DBCDatabaseContainerProvider.java | 29 ++++++++ .../MySQLR2DBCDatabaseContainerTest.java | 69 +++++++++++++++++++ .../mysql/src/test/resources/logback-test.xml | 16 +++++ 5 files changed, 156 insertions(+) create mode 100644 modules/mysql/src/main/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainer.java create mode 100644 modules/mysql/src/main/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerProvider.java create mode 100644 modules/mysql/src/test/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerTest.java create mode 100644 modules/mysql/src/test/resources/logback-test.xml diff --git a/modules/mysql/build.gradle b/modules/mysql/build.gradle index 11c522197b7..9eadf38e902 100644 --- a/modules/mysql/build.gradle +++ b/modules/mysql/build.gradle @@ -1,11 +1,20 @@ description = "Testcontainers :: JDBC :: MySQL" dependencies { + annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6' + compileOnly 'com.google.auto.service:auto-service:1.0-rc6' + compile project(':jdbc') + compileOnly project(':r2dbc') + compileOnly 'dev.miku:r2dbc-mysql:0.8.1.RELEASE' + testCompile 'mysql:mysql-connector-java:8.0.19' testCompile 'com.zaxxer:HikariCP-java6:2.3.13' testCompile 'org.apache.tomcat:tomcat-jdbc:9.0.31' testCompile 'commons-dbutils:commons-dbutils:1.7' testCompile 'org.vibur:vibur-dbcp:25.0' + + testCompile project(':r2dbc') + testCompile 'dev.miku:r2dbc-mysql:0.8.1.RELEASE' } diff --git a/modules/mysql/src/main/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainer.java b/modules/mysql/src/main/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainer.java new file mode 100644 index 00000000000..c43ffcca1b2 --- /dev/null +++ b/modules/mysql/src/main/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainer.java @@ -0,0 +1,33 @@ +package org.testcontainers.containers; + +import io.r2dbc.spi.ConnectionFactoryOptions; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Delegate; +import org.testcontainers.lifecycle.Startable; +import org.testcontainers.r2dbc.R2DBCDatabaseContainer; + +@RequiredArgsConstructor +public class MySQLR2DBCDatabaseContainer implements R2DBCDatabaseContainer { + + @Delegate(types = Startable.class) + private final MySQLContainer container; + + public static ConnectionFactoryOptions getOptions(MySQLContainer container) { + ConnectionFactoryOptions options = ConnectionFactoryOptions.builder() + .option(ConnectionFactoryOptions.DRIVER, MySQLR2DBCDatabaseContainerProvider.DRIVER) + .build(); + + return new MySQLR2DBCDatabaseContainer(container).configure(options); + } + + @Override + public ConnectionFactoryOptions configure(ConnectionFactoryOptions options) { + return options.mutate() + .option(ConnectionFactoryOptions.HOST, container.getContainerIpAddress()) + .option(ConnectionFactoryOptions.PORT, container.getMappedPort(MySQLContainer.MYSQL_PORT)) + .option(ConnectionFactoryOptions.DATABASE, container.getDatabaseName()) + .option(ConnectionFactoryOptions.USER, container.getUsername()) + .option(ConnectionFactoryOptions.PASSWORD, container.getPassword()) + .build(); + } +} diff --git a/modules/mysql/src/main/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerProvider.java b/modules/mysql/src/main/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerProvider.java new file mode 100644 index 00000000000..c6aa5e323d1 --- /dev/null +++ b/modules/mysql/src/main/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerProvider.java @@ -0,0 +1,29 @@ +package org.testcontainers.containers; + +import com.google.auto.service.AutoService; +import dev.miku.r2dbc.mysql.MySqlConnectionFactoryProvider; +import io.r2dbc.spi.ConnectionFactoryOptions; +import org.testcontainers.r2dbc.R2DBCDatabaseContainer; +import org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider; + +@AutoService(R2DBCDatabaseContainerProvider.class) +public class MySQLR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider { + + static final String DRIVER = MySqlConnectionFactoryProvider.MYSQL_DRIVER; + + @Override + public boolean supports(ConnectionFactoryOptions options) { + return DRIVER.equals(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)); + } + + @Override + public R2DBCDatabaseContainer createContainer(ConnectionFactoryOptions options) { + MySQLContainer container = new MySQLContainer<>(options.getRequiredValue(IMAGE_OPTION)) + .withDatabaseName(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)); + + if (Boolean.TRUE.equals(options.getValue(REUSABLE_OPTION))) { + container.withReuse(true); + } + return new MySQLR2DBCDatabaseContainer(container); + } +} diff --git a/modules/mysql/src/test/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerTest.java b/modules/mysql/src/test/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerTest.java new file mode 100644 index 00000000000..42ac730b5af --- /dev/null +++ b/modules/mysql/src/test/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerTest.java @@ -0,0 +1,69 @@ +package org.testcontainers.containers; + +import io.r2dbc.spi.Closeable; +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactories; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.Result; +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import static org.junit.Assert.*; + +public class MySQLR2DBCDatabaseContainerTest { + + @Test + public void testGetOptions() { + try (MySQLContainer container = new MySQLContainer<>()) { + container.start(); + + long result = Flux + .usingWhen( + Mono.just( + ConnectionFactories.get( + MySQLR2DBCDatabaseContainer.getOptions(container) + ) + ), + connectionFactory -> { + return Flux + .usingWhen( + connectionFactory.create(), + connection -> connection.createStatement("SELECT 42").execute(), + Connection::close + ) + .flatMap(it -> it.map((row, meta) -> (Long) row.get(0))); + }, + it -> ((Closeable) it).close() + ) + .blockFirst(); + + assertEquals(42, result); + } + } + + @Test + public void testUrlSupport() { + String url = "r2dbc:tc:mysql:///db?TC_IMAGE=mysql:5.7.22"; + ConnectionFactory connectionFactory = ConnectionFactories.get(url); + try { + int updated = Flux + .usingWhen( + connectionFactory.create(), + connection -> { + return Mono + .from(connection.createStatement("CREATE TABLE test(id integer PRIMARY KEY)").execute()) + .thenMany(connection.createStatement("INSERT INTO test(id) VALUES(123)").execute()) + .flatMap(Result::getRowsUpdated); + }, + Connection::close + ) + .blockFirst(); + + assertEquals(updated, 1); + } finally { + Mono.from(((Closeable) connectionFactory).close()).block(); + } + } + +} diff --git a/modules/mysql/src/test/resources/logback-test.xml b/modules/mysql/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..535e406fc13 --- /dev/null +++ b/modules/mysql/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + From a3a5c5a21a9c17b8933e92fd9e1b554e3fc965a5 Mon Sep 17 00:00:00 2001 From: Sergei Egorov Date: Thu, 9 Apr 2020 17:08:01 +0200 Subject: [PATCH 07/15] Add MariaDB --- modules/mariadb/build.gradle | 9 +++ .../containers/MariaDBContainer.java | 4 +- .../MariaDBR2DBCDatabaseContainer.java | 33 +++++++++ ...MariaDBR2DBCDatabaseContainerProvider.java | 29 ++++++++ .../MariaDBR2DBCDatabaseContainerTest.java | 69 +++++++++++++++++++ .../src/test/resources/logback-test.xml | 16 +++++ 6 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainer.java create mode 100644 modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerProvider.java create mode 100644 modules/mariadb/src/test/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerTest.java create mode 100644 modules/mariadb/src/test/resources/logback-test.xml diff --git a/modules/mariadb/build.gradle b/modules/mariadb/build.gradle index c479824ae47..7084da46d00 100644 --- a/modules/mariadb/build.gradle +++ b/modules/mariadb/build.gradle @@ -1,11 +1,20 @@ description = "Testcontainers :: JDBC :: MariaDB" dependencies { + annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6' + compileOnly 'com.google.auto.service:auto-service:1.0-rc6' + compile project(':jdbc') + compileOnly project(':r2dbc') + compileOnly 'org.mariadb:r2dbc-mariadb:0.8.1-alpha1' + testCompile 'org.mariadb.jdbc:mariadb-java-client:2.5.4' testCompile 'com.zaxxer:HikariCP-java6:2.3.13' testCompile 'commons-dbutils:commons-dbutils:1.7' testCompile 'org.apache.tomcat:tomcat-jdbc:9.0.31' testCompile 'org.vibur:vibur-dbcp:25.0' + + testCompile project(':r2dbc') + testCompile 'org.mariadb:r2dbc-mariadb:0.8.1-alpha1' } diff --git a/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBContainer.java b/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBContainer.java index c41136c0bf2..3b6c8e63a16 100644 --- a/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBContainer.java +++ b/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBContainer.java @@ -11,7 +11,7 @@ public class MariaDBContainer> extends JdbcD public static final String IMAGE = "mariadb"; public static final String DEFAULT_TAG = "10.3.6"; - private static final Integer MARIADB_PORT = 3306; + static final Integer MARIADB_PORT = 3306; private String databaseName = "test"; private String username = "test"; private String password = "test"; @@ -83,7 +83,7 @@ public SELF withConfigurationOverride(String s) { parameters.put(MY_CNF_CONFIG_OVERRIDE_PARAM_NAME, s); return self(); } - + @Override public SELF withDatabaseName(final String databaseName) { this.databaseName = databaseName; diff --git a/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainer.java b/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainer.java new file mode 100644 index 00000000000..4ddbed997d4 --- /dev/null +++ b/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainer.java @@ -0,0 +1,33 @@ +package org.testcontainers.containers; + +import io.r2dbc.spi.ConnectionFactoryOptions; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Delegate; +import org.testcontainers.lifecycle.Startable; +import org.testcontainers.r2dbc.R2DBCDatabaseContainer; + +@RequiredArgsConstructor +public class MariaDBR2DBCDatabaseContainer implements R2DBCDatabaseContainer { + + @Delegate(types = Startable.class) + private final MariaDBContainer container; + + public static ConnectionFactoryOptions getOptions(MariaDBContainer container) { + ConnectionFactoryOptions options = ConnectionFactoryOptions.builder() + .option(ConnectionFactoryOptions.DRIVER, MariaDBR2DBCDatabaseContainerProvider.DRIVER) + .build(); + + return new MariaDBR2DBCDatabaseContainer(container).configure(options); + } + + @Override + public ConnectionFactoryOptions configure(ConnectionFactoryOptions options) { + return options.mutate() + .option(ConnectionFactoryOptions.HOST, container.getContainerIpAddress()) + .option(ConnectionFactoryOptions.PORT, container.getMappedPort(MariaDBContainer.MARIADB_PORT)) + .option(ConnectionFactoryOptions.DATABASE, container.getDatabaseName()) + .option(ConnectionFactoryOptions.USER, container.getUsername()) + .option(ConnectionFactoryOptions.PASSWORD, container.getPassword()) + .build(); + } +} diff --git a/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerProvider.java b/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerProvider.java new file mode 100644 index 00000000000..f0da0fb4fe4 --- /dev/null +++ b/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerProvider.java @@ -0,0 +1,29 @@ +package org.testcontainers.containers; + +import com.google.auto.service.AutoService; +import io.r2dbc.spi.ConnectionFactoryOptions; +import org.mariadb.r2dbc.MariadbConnectionFactoryProvider; +import org.testcontainers.r2dbc.R2DBCDatabaseContainer; +import org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider; + +@AutoService(R2DBCDatabaseContainerProvider.class) +public class MariaDBR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider { + + static final String DRIVER = MariadbConnectionFactoryProvider.MARIADB_DRIVER; + + @Override + public boolean supports(ConnectionFactoryOptions options) { + return DRIVER.equals(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)); + } + + @Override + public R2DBCDatabaseContainer createContainer(ConnectionFactoryOptions options) { + MariaDBContainer container = new MariaDBContainer<>(options.getRequiredValue(IMAGE_OPTION)) + .withDatabaseName(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)); + + if (Boolean.TRUE.equals(options.getValue(REUSABLE_OPTION))) { + container.withReuse(true); + } + return new MariaDBR2DBCDatabaseContainer(container); + } +} diff --git a/modules/mariadb/src/test/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerTest.java b/modules/mariadb/src/test/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerTest.java new file mode 100644 index 00000000000..f66706c3c03 --- /dev/null +++ b/modules/mariadb/src/test/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerTest.java @@ -0,0 +1,69 @@ +package org.testcontainers.containers; + +import io.r2dbc.spi.Closeable; +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactories; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.Result; +import org.junit.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import static org.junit.Assert.*; + +public class MariaDBR2DBCDatabaseContainerTest { + + @Test + public void testGetOptions() { + try (MariaDBContainer container = new MariaDBContainer<>()) { + container.start(); + + int result = Flux + .usingWhen( + Mono.just( + ConnectionFactories.get( + MariaDBR2DBCDatabaseContainer.getOptions(container) + ) + ), + connectionFactory -> { + return Flux + .usingWhen( + connectionFactory.create(), + connection -> connection.createStatement("SELECT 42").execute(), + Connection::close + ) + .flatMap(it -> it.map((row, meta) -> (Integer) row.get(0))); + }, + it -> ((Closeable) it).close() + ) + .blockFirst(); + + assertEquals(42, result); + } + } + + @Test + public void testUrlSupport() { + String url = "r2dbc:tc:mariadb:///db?TC_IMAGE=mysql:5.7.22"; + ConnectionFactory connectionFactory = ConnectionFactories.get(url); + try { + int updated = Flux + .usingWhen( + connectionFactory.create(), + connection -> { + return Mono + .from(connection.createStatement("CREATE TABLE test(id integer PRIMARY KEY)").execute()) + .thenMany(connection.createStatement("INSERT INTO test(id) VALUES(123)").execute()) + .flatMap(Result::getRowsUpdated); + }, + Connection::close + ) + .blockFirst(); + + assertEquals(updated, 1); + } finally { + Mono.from(((Closeable) connectionFactory).close()).block(); + } + } + +} diff --git a/modules/mariadb/src/test/resources/logback-test.xml b/modules/mariadb/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..535e406fc13 --- /dev/null +++ b/modules/mariadb/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + From 886c9e725befd219c1906fc2ddda2338061c945a Mon Sep 17 00:00:00 2001 From: Sergei Egorov Date: Sat, 11 Apr 2020 12:39:48 +0200 Subject: [PATCH 08/15] Use `@AutoService` on `TestcontainersR2DBCConnectionFactoryProvider` --- modules/r2dbc/build.gradle | 3 +++ .../r2dbc/src/main/java/org/testcontainers/r2dbc/Hidden.java | 3 +++ .../META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider | 1 - 3 files changed, 6 insertions(+), 1 deletion(-) delete mode 100644 modules/r2dbc/src/main/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider diff --git a/modules/r2dbc/build.gradle b/modules/r2dbc/build.gradle index 0914e951ce2..6589619d317 100644 --- a/modules/r2dbc/build.gradle +++ b/modules/r2dbc/build.gradle @@ -1,6 +1,9 @@ description = "Testcontainers :: R2DBC" dependencies { + annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6' + compileOnly 'com.google.auto.service:auto-service:1.0-rc6' + compile project(':testcontainers') compile 'io.r2dbc:r2dbc-spi:0.8.1.RELEASE' diff --git a/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/Hidden.java b/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/Hidden.java index 78d5e7b9b35..f642a60d67c 100644 --- a/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/Hidden.java +++ b/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/Hidden.java @@ -1,5 +1,6 @@ package org.testcontainers.r2dbc; +import com.google.auto.service.AutoService; import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.ConnectionFactoryOptions; import io.r2dbc.spi.ConnectionFactoryProvider; @@ -8,6 +9,8 @@ * Hide inner classes that must be public due to the way {@link java.util.ServiceLoader} works */ class Hidden { + + @AutoService(ConnectionFactoryProvider.class) public static final class TestcontainersR2DBCConnectionFactoryProvider implements ConnectionFactoryProvider { public static final String DRIVER = "tc"; diff --git a/modules/r2dbc/src/main/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider b/modules/r2dbc/src/main/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider deleted file mode 100644 index 23f1702c57a..00000000000 --- a/modules/r2dbc/src/main/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider +++ /dev/null @@ -1 +0,0 @@ -org.testcontainers.r2dbc.Hidden$TestcontainersR2DBCConnectionFactoryProvider From 834dae85d7df4032b4a378afc8ba3b90221d2833 Mon Sep 17 00:00:00 2001 From: Sergei Egorov Date: Sun, 12 Apr 2020 12:26:05 +0200 Subject: [PATCH 09/15] add `AbstractR2DBCDatabaseContainerTest` --- modules/mariadb/build.gradle | 2 +- .../MariaDBR2DBCDatabaseContainerTest.java | 72 ++++--------------- modules/mssqlserver/build.gradle | 1 + .../MSSQLR2DBCDatabaseContainerTest.java | 66 ++++------------- modules/mysql/build.gradle | 2 +- .../MySQLR2DBCDatabaseContainerTest.java | 72 ++++--------------- modules/postgresql/build.gradle | 2 +- .../PostgreSQLR2DBCDatabaseContainerTest.java | 71 ++++-------------- modules/r2dbc/build.gradle | 7 ++ .../AbstractR2DBCDatabaseContainerTest.java | 64 +++++++++++++++++ 10 files changed, 126 insertions(+), 233 deletions(-) create mode 100644 modules/r2dbc/src/testFixtures/java/org/testcontainers/r2dbc/AbstractR2DBCDatabaseContainerTest.java diff --git a/modules/mariadb/build.gradle b/modules/mariadb/build.gradle index bb9aeea4b7c..93b2de2a716 100644 --- a/modules/mariadb/build.gradle +++ b/modules/mariadb/build.gradle @@ -15,6 +15,6 @@ dependencies { testCompile 'org.apache.tomcat:tomcat-jdbc:9.0.33' testCompile 'org.vibur:vibur-dbcp:25.0' - testCompile project(':r2dbc') + testCompile testFixtures(project(':r2dbc')) testCompile 'org.mariadb:r2dbc-mariadb:0.8.1-alpha1' } diff --git a/modules/mariadb/src/test/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerTest.java b/modules/mariadb/src/test/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerTest.java index f66706c3c03..9a63b1eefd2 100644 --- a/modules/mariadb/src/test/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerTest.java +++ b/modules/mariadb/src/test/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerTest.java @@ -1,69 +1,23 @@ package org.testcontainers.containers; -import io.r2dbc.spi.Closeable; -import io.r2dbc.spi.Connection; -import io.r2dbc.spi.ConnectionFactories; -import io.r2dbc.spi.ConnectionFactory; -import io.r2dbc.spi.Result; -import org.junit.Test; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; +import io.r2dbc.spi.ConnectionFactoryOptions; +import org.testcontainers.r2dbc.AbstractR2DBCDatabaseContainerTest; -import static org.junit.Assert.*; +public class MariaDBR2DBCDatabaseContainerTest extends AbstractR2DBCDatabaseContainerTest> { -public class MariaDBR2DBCDatabaseContainerTest { - - @Test - public void testGetOptions() { - try (MariaDBContainer container = new MariaDBContainer<>()) { - container.start(); - - int result = Flux - .usingWhen( - Mono.just( - ConnectionFactories.get( - MariaDBR2DBCDatabaseContainer.getOptions(container) - ) - ), - connectionFactory -> { - return Flux - .usingWhen( - connectionFactory.create(), - connection -> connection.createStatement("SELECT 42").execute(), - Connection::close - ) - .flatMap(it -> it.map((row, meta) -> (Integer) row.get(0))); - }, - it -> ((Closeable) it).close() - ) - .blockFirst(); - - assertEquals(42, result); - } + @Override + protected ConnectionFactoryOptions getOptions(MariaDBContainer container) { + return MariaDBR2DBCDatabaseContainer.getOptions(container); } - @Test - public void testUrlSupport() { - String url = "r2dbc:tc:mariadb:///db?TC_IMAGE=mysql:5.7.22"; - ConnectionFactory connectionFactory = ConnectionFactories.get(url); - try { - int updated = Flux - .usingWhen( - connectionFactory.create(), - connection -> { - return Mono - .from(connection.createStatement("CREATE TABLE test(id integer PRIMARY KEY)").execute()) - .thenMany(connection.createStatement("INSERT INTO test(id) VALUES(123)").execute()) - .flatMap(Result::getRowsUpdated); - }, - Connection::close - ) - .blockFirst(); + @Override + protected String createR2DBCUrl() { + return "r2dbc:tc:mariadb:///db?TC_IMAGE=mariadb:10.3.6"; + } - assertEquals(updated, 1); - } finally { - Mono.from(((Closeable) connectionFactory).close()).block(); - } + @Override + protected MariaDBContainer createContainer() { + return new MariaDBContainer<>(); } } diff --git a/modules/mssqlserver/build.gradle b/modules/mssqlserver/build.gradle index 19de3eca1a7..3c44df64eda 100644 --- a/modules/mssqlserver/build.gradle +++ b/modules/mssqlserver/build.gradle @@ -13,5 +13,6 @@ dependencies { testCompile 'io.r2dbc:r2dbc-mssql:0.8.1.RELEASE' // MSSQL's wait strategy requires the JDBC driver + testCompile testFixtures(project(':r2dbc')) testCompile 'com.microsoft.sqlserver:mssql-jdbc:6.1.0.jre8' } diff --git a/modules/mssqlserver/src/test/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerTest.java b/modules/mssqlserver/src/test/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerTest.java index 922176aaeb7..19ebfd28807 100644 --- a/modules/mssqlserver/src/test/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerTest.java +++ b/modules/mssqlserver/src/test/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerTest.java @@ -1,62 +1,22 @@ package org.testcontainers.containers; -import io.r2dbc.spi.Closeable; -import io.r2dbc.spi.Connection; -import io.r2dbc.spi.ConnectionFactories; -import io.r2dbc.spi.ConnectionFactory; -import io.r2dbc.spi.Result; -import org.junit.Test; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; +import io.r2dbc.spi.ConnectionFactoryOptions; +import org.testcontainers.r2dbc.AbstractR2DBCDatabaseContainerTest; -import static org.junit.Assert.*; +public class MSSQLR2DBCDatabaseContainerTest extends AbstractR2DBCDatabaseContainerTest> { -public class MSSQLR2DBCDatabaseContainerTest { - - @Test - public void testGetOptions() { - try (MSSQLServerContainer container = new MSSQLServerContainer<>()) { - container.start(); - - ConnectionFactory connectionFactory = ConnectionFactories.get( - MSSQLR2DBCDatabaseContainer.getOptions(container) - ); - - int result = Flux - .usingWhen( - connectionFactory.create(), - connection -> connection.createStatement("SELECT 42").execute(), - Connection::close - ) - .flatMap(it -> it.map((row, meta) -> (Integer) row.get(0))) - .blockFirst(); - - assertEquals(42, result); - } + @Override + protected ConnectionFactoryOptions getOptions(MSSQLServerContainer container) { + return MSSQLR2DBCDatabaseContainer.getOptions(container); } - @Test - public void testUrlSupport() { - String url = "r2dbc:tc:sqlserver:///?TC_IMAGE=mcr.microsoft.com%2Fmssql%2Fserver%3A2017-CU12"; - ConnectionFactory connectionFactory = ConnectionFactories.get(url); - try { - int updated = Flux - .usingWhen( - connectionFactory.create(), - connection -> { - return Mono - .from(connection.createStatement("CREATE DATABASE [test];").execute()) - .thenMany(connection.createStatement("CREATE TABLE test(id integer PRIMARY KEY)").execute()) - .thenMany(connection.createStatement("INSERT INTO test(id) VALUES(123)").execute()) - .flatMap(Result::getRowsUpdated); - }, - Connection::close - ) - .blockFirst(); + @Override + protected String createR2DBCUrl() { + return "r2dbc:tc:sqlserver:///?TC_IMAGE=mcr.microsoft.com%2Fmssql%2Fserver%3A2017-CU12"; + } - assertEquals(updated, 1); - } finally { - Mono.from(((Closeable) connectionFactory).close()).block(); - } + @Override + protected MSSQLServerContainer createContainer() { + return new MSSQLServerContainer<>(); } } diff --git a/modules/mysql/build.gradle b/modules/mysql/build.gradle index 22140878da5..10fd90b3c38 100644 --- a/modules/mysql/build.gradle +++ b/modules/mysql/build.gradle @@ -15,6 +15,6 @@ dependencies { testCompile 'commons-dbutils:commons-dbutils:1.7' testCompile 'org.vibur:vibur-dbcp:25.0' - testCompile project(':r2dbc') + testCompile testFixtures(project(':r2dbc')) testCompile 'dev.miku:r2dbc-mysql:0.8.1.RELEASE' } diff --git a/modules/mysql/src/test/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerTest.java b/modules/mysql/src/test/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerTest.java index 42ac730b5af..cc6d3b8fd8f 100644 --- a/modules/mysql/src/test/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerTest.java +++ b/modules/mysql/src/test/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerTest.java @@ -1,69 +1,23 @@ package org.testcontainers.containers; -import io.r2dbc.spi.Closeable; -import io.r2dbc.spi.Connection; -import io.r2dbc.spi.ConnectionFactories; -import io.r2dbc.spi.ConnectionFactory; -import io.r2dbc.spi.Result; -import org.junit.Test; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; +import io.r2dbc.spi.ConnectionFactoryOptions; +import org.testcontainers.r2dbc.AbstractR2DBCDatabaseContainerTest; -import static org.junit.Assert.*; +public class MySQLR2DBCDatabaseContainerTest extends AbstractR2DBCDatabaseContainerTest> { -public class MySQLR2DBCDatabaseContainerTest { - - @Test - public void testGetOptions() { - try (MySQLContainer container = new MySQLContainer<>()) { - container.start(); - - long result = Flux - .usingWhen( - Mono.just( - ConnectionFactories.get( - MySQLR2DBCDatabaseContainer.getOptions(container) - ) - ), - connectionFactory -> { - return Flux - .usingWhen( - connectionFactory.create(), - connection -> connection.createStatement("SELECT 42").execute(), - Connection::close - ) - .flatMap(it -> it.map((row, meta) -> (Long) row.get(0))); - }, - it -> ((Closeable) it).close() - ) - .blockFirst(); - - assertEquals(42, result); - } + @Override + protected ConnectionFactoryOptions getOptions(MySQLContainer container) { + return MySQLR2DBCDatabaseContainer.getOptions(container); } - @Test - public void testUrlSupport() { - String url = "r2dbc:tc:mysql:///db?TC_IMAGE=mysql:5.7.22"; - ConnectionFactory connectionFactory = ConnectionFactories.get(url); - try { - int updated = Flux - .usingWhen( - connectionFactory.create(), - connection -> { - return Mono - .from(connection.createStatement("CREATE TABLE test(id integer PRIMARY KEY)").execute()) - .thenMany(connection.createStatement("INSERT INTO test(id) VALUES(123)").execute()) - .flatMap(Result::getRowsUpdated); - }, - Connection::close - ) - .blockFirst(); + @Override + protected String createR2DBCUrl() { + return "r2dbc:tc:mysql:///db?TC_IMAGE=mysql:5.7.22"; + } - assertEquals(updated, 1); - } finally { - Mono.from(((Closeable) connectionFactory).close()).block(); - } + @Override + protected MySQLContainer createContainer() { + return new MySQLContainer<>(); } } diff --git a/modules/postgresql/build.gradle b/modules/postgresql/build.gradle index 5830d188762..c629c1c5a13 100644 --- a/modules/postgresql/build.gradle +++ b/modules/postgresql/build.gradle @@ -12,6 +12,6 @@ dependencies { testCompile 'commons-dbutils:commons-dbutils:1.7' testCompile 'com.zaxxer:HikariCP-java6:2.3.13' - testCompile project(':r2dbc') + testCompile testFixtures(project(':r2dbc')) testCompile 'io.r2dbc:r2dbc-postgresql:0.8.1.RELEASE' } diff --git a/modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerTest.java b/modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerTest.java index 8e937b405d7..66b5a85b058 100644 --- a/modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerTest.java +++ b/modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerTest.java @@ -1,68 +1,21 @@ package org.testcontainers.containers; -import io.r2dbc.spi.Closeable; -import io.r2dbc.spi.Connection; -import io.r2dbc.spi.ConnectionFactories; -import io.r2dbc.spi.ConnectionFactory; -import io.r2dbc.spi.Result; -import org.junit.Test; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; +import io.r2dbc.spi.ConnectionFactoryOptions; +import org.testcontainers.r2dbc.AbstractR2DBCDatabaseContainerTest; -import static org.junit.Assert.*; +public class PostgreSQLR2DBCDatabaseContainerTest extends AbstractR2DBCDatabaseContainerTest> { -public class PostgreSQLR2DBCDatabaseContainerTest { - - @Test - public void testGetOptions() { - try (PostgreSQLContainer container = new PostgreSQLContainer<>()) { - container.start(); - - int result = Flux - .usingWhen( - Mono.just( - ConnectionFactories.get( - PostgreSQLR2DBCDatabaseContainer.getOptions(container) - ) - ), - connectionFactory -> { - return Flux - .usingWhen( - connectionFactory.create(), - connection -> connection.createStatement("SELECT 42").execute(), - Connection::close - ) - .flatMap(it -> it.map((row, meta) -> (Integer) row.get(0))); - }, - it -> ((Closeable) it).close() - ) - .blockFirst(); - - assertEquals(42, result); - } + @Override + protected PostgreSQLContainer createContainer() { + return new PostgreSQLContainer<>(); } - @Test - public void testUrlSupport() { - String url = "r2dbc:tc:postgresql:///db?TC_IMAGE=postgres:10-alpine"; - ConnectionFactory connectionFactory = ConnectionFactories.get(url); - try { - int updated = Flux - .usingWhen( - connectionFactory.create(), - connection -> { - return Mono - .from(connection.createStatement("CREATE TABLE test(id integer PRIMARY KEY)").execute()) - .thenMany(connection.createStatement("INSERT INTO test(id) VALUES(123)").execute()) - .flatMap(Result::getRowsUpdated); - }, - Connection::close - ) - .blockFirst(); + @Override + protected ConnectionFactoryOptions getOptions(PostgreSQLContainer container) { + return PostgreSQLR2DBCDatabaseContainer.getOptions(container); + } - assertEquals(updated, 1); - } finally { - Mono.from(((Closeable) connectionFactory).close()).block(); - } + protected String createR2DBCUrl() { + return "r2dbc:tc:postgresql:///db?TC_IMAGE=postgres:10-alpine"; } } diff --git a/modules/r2dbc/build.gradle b/modules/r2dbc/build.gradle index 6589619d317..a7388ebc6d2 100644 --- a/modules/r2dbc/build.gradle +++ b/modules/r2dbc/build.gradle @@ -1,3 +1,7 @@ +plugins { + id "java-test-fixtures" +} + description = "Testcontainers :: R2DBC" dependencies { @@ -10,4 +14,7 @@ dependencies { testCompile 'org.assertj:assertj-core:3.14.0' testCompile 'io.r2dbc:r2dbc-postgresql:0.8.1.RELEASE' testCompile project(':postgresql') + + testFixturesCompile 'io.projectreactor:reactor-core:3.3.4.RELEASE' + testFixturesCompile 'org.assertj:assertj-core:3.14.0' } diff --git a/modules/r2dbc/src/testFixtures/java/org/testcontainers/r2dbc/AbstractR2DBCDatabaseContainerTest.java b/modules/r2dbc/src/testFixtures/java/org/testcontainers/r2dbc/AbstractR2DBCDatabaseContainerTest.java new file mode 100644 index 00000000000..7f14ecca2a7 --- /dev/null +++ b/modules/r2dbc/src/testFixtures/java/org/testcontainers/r2dbc/AbstractR2DBCDatabaseContainerTest.java @@ -0,0 +1,64 @@ +package org.testcontainers.r2dbc; + +import io.r2dbc.spi.Closeable; +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactories; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryOptions; +import org.junit.Test; +import org.testcontainers.containers.GenericContainer; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import static org.assertj.core.api.Assertions.assertThat; + +public abstract class AbstractR2DBCDatabaseContainerTest> { + + protected abstract ConnectionFactoryOptions getOptions(T container); + + protected abstract String createR2DBCUrl(); + + protected String createTestQuery(int result) { + return String.format("SELECT %d", result); + } + + @Test + public final void testGetOptions() { + try (T container = createContainer()) { + container.start(); + + ConnectionFactory connectionFactory = ConnectionFactories.get(getOptions(container)); + runTestQuery(connectionFactory); + } + } + + @Test + public final void testUrlSupport() { + ConnectionFactory connectionFactory = ConnectionFactories.get(createR2DBCUrl()); + runTestQuery(connectionFactory); + } + + protected abstract T createContainer(); + + protected void runTestQuery(ConnectionFactory connectionFactory) { + try { + int expected = 42; + Number result = Flux + .usingWhen( + connectionFactory.create(), + connection -> connection.createStatement(createTestQuery(expected)).execute(), + Connection::close + ) + .flatMap(it -> it.map((row, meta) -> (Number) row.get(0))) + .blockFirst(); + + assertThat(result) + .isNotNull() + .returns(expected, Number::intValue); + } finally { + if (connectionFactory instanceof Closeable) { + Mono.from(((Closeable) connectionFactory).close()).block(); + } + } + } +} From 937fe9603dd55912051f44910b5ecd75be6a2a59 Mon Sep 17 00:00:00 2001 From: Sergei Egorov Date: Sun, 12 Apr 2020 12:43:15 +0200 Subject: [PATCH 10/15] `TC_IMAGE` -> `TC_IMAGE_TAG` --- .../containers/MariaDBR2DBCDatabaseContainerProvider.java | 3 ++- .../containers/MariaDBR2DBCDatabaseContainerTest.java | 2 +- .../containers/MSSQLR2DBCDatabaseContainerProvider.java | 3 ++- .../containers/MSSQLR2DBCDatabaseContainerTest.java | 2 +- .../containers/MySQLR2DBCDatabaseContainerProvider.java | 3 ++- .../containers/MySQLR2DBCDatabaseContainerTest.java | 2 +- .../containers/PostgreSQLR2DBCDatabaseContainerProvider.java | 3 ++- .../containers/PostgreSQLR2DBCDatabaseContainerTest.java | 2 +- .../testcontainers/r2dbc/R2DBCDatabaseContainerProvider.java | 2 +- 9 files changed, 13 insertions(+), 9 deletions(-) diff --git a/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerProvider.java b/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerProvider.java index f0da0fb4fe4..79cb627f8a9 100644 --- a/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerProvider.java +++ b/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerProvider.java @@ -18,7 +18,8 @@ public boolean supports(ConnectionFactoryOptions options) { @Override public R2DBCDatabaseContainer createContainer(ConnectionFactoryOptions options) { - MariaDBContainer container = new MariaDBContainer<>(options.getRequiredValue(IMAGE_OPTION)) + String image = MariaDBContainer.IMAGE + ":" + options.getRequiredValue(IMAGE_TAG_OPTION); + MariaDBContainer container = new MariaDBContainer<>(image) .withDatabaseName(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)); if (Boolean.TRUE.equals(options.getValue(REUSABLE_OPTION))) { diff --git a/modules/mariadb/src/test/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerTest.java b/modules/mariadb/src/test/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerTest.java index 9a63b1eefd2..30e8cad9c32 100644 --- a/modules/mariadb/src/test/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerTest.java +++ b/modules/mariadb/src/test/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerTest.java @@ -12,7 +12,7 @@ protected ConnectionFactoryOptions getOptions(MariaDBContainer container) { @Override protected String createR2DBCUrl() { - return "r2dbc:tc:mariadb:///db?TC_IMAGE=mariadb:10.3.6"; + return "r2dbc:tc:mariadb:///db?TC_IMAGE_TAG=10.3.6"; } @Override diff --git a/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerProvider.java b/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerProvider.java index dd03be633c6..1638901226c 100644 --- a/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerProvider.java +++ b/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerProvider.java @@ -18,7 +18,8 @@ public boolean supports(ConnectionFactoryOptions options) { @Override public R2DBCDatabaseContainer createContainer(ConnectionFactoryOptions options) { - MSSQLServerContainer container = new MSSQLServerContainer<>(options.getRequiredValue(IMAGE_OPTION)); + String image = MSSQLServerContainer.IMAGE + ":" + options.getRequiredValue(IMAGE_TAG_OPTION); + MSSQLServerContainer container = new MSSQLServerContainer<>(image); if (Boolean.TRUE.equals(options.getValue(REUSABLE_OPTION))) { container.withReuse(true); diff --git a/modules/mssqlserver/src/test/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerTest.java b/modules/mssqlserver/src/test/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerTest.java index 19ebfd28807..1fcf2dd293b 100644 --- a/modules/mssqlserver/src/test/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerTest.java +++ b/modules/mssqlserver/src/test/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerTest.java @@ -12,7 +12,7 @@ protected ConnectionFactoryOptions getOptions(MSSQLServerContainer container) @Override protected String createR2DBCUrl() { - return "r2dbc:tc:sqlserver:///?TC_IMAGE=mcr.microsoft.com%2Fmssql%2Fserver%3A2017-CU12"; + return "r2dbc:tc:sqlserver:///?TC_IMAGE_TAG=2017-CU12"; } @Override diff --git a/modules/mysql/src/main/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerProvider.java b/modules/mysql/src/main/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerProvider.java index c6aa5e323d1..3a01616caba 100644 --- a/modules/mysql/src/main/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerProvider.java +++ b/modules/mysql/src/main/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerProvider.java @@ -18,7 +18,8 @@ public boolean supports(ConnectionFactoryOptions options) { @Override public R2DBCDatabaseContainer createContainer(ConnectionFactoryOptions options) { - MySQLContainer container = new MySQLContainer<>(options.getRequiredValue(IMAGE_OPTION)) + String image = MySQLContainer.IMAGE + ":" + options.getRequiredValue(IMAGE_TAG_OPTION); + MySQLContainer container = new MySQLContainer<>(image) .withDatabaseName(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)); if (Boolean.TRUE.equals(options.getValue(REUSABLE_OPTION))) { diff --git a/modules/mysql/src/test/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerTest.java b/modules/mysql/src/test/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerTest.java index cc6d3b8fd8f..19edc3dbca4 100644 --- a/modules/mysql/src/test/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerTest.java +++ b/modules/mysql/src/test/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerTest.java @@ -12,7 +12,7 @@ protected ConnectionFactoryOptions getOptions(MySQLContainer container) { @Override protected String createR2DBCUrl() { - return "r2dbc:tc:mysql:///db?TC_IMAGE=mysql:5.7.22"; + return "r2dbc:tc:mysql:///db?TC_IMAGE_TAG=5.7.22"; } @Override diff --git a/modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerProvider.java b/modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerProvider.java index d716e334403..5e9da980b8e 100644 --- a/modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerProvider.java +++ b/modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerProvider.java @@ -18,7 +18,8 @@ public boolean supports(ConnectionFactoryOptions options) { @Override public R2DBCDatabaseContainer createContainer(ConnectionFactoryOptions options) { - PostgreSQLContainer container = new PostgreSQLContainer<>(options.getRequiredValue(IMAGE_OPTION)) + String image = PostgreSQLContainer.IMAGE + ":" + options.getRequiredValue(IMAGE_TAG_OPTION); + PostgreSQLContainer container = new PostgreSQLContainer<>(image) .withDatabaseName(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)); if (Boolean.TRUE.equals(options.getValue(REUSABLE_OPTION))) { diff --git a/modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerTest.java b/modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerTest.java index 66b5a85b058..ab692ef5275 100644 --- a/modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerTest.java +++ b/modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerTest.java @@ -16,6 +16,6 @@ protected ConnectionFactoryOptions getOptions(PostgreSQLContainer container) } protected String createR2DBCUrl() { - return "r2dbc:tc:postgresql:///db?TC_IMAGE=postgres:10-alpine"; + return "r2dbc:tc:postgresql:///db?TC_IMAGE_TAG=10-alpine"; } } diff --git a/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/R2DBCDatabaseContainerProvider.java b/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/R2DBCDatabaseContainerProvider.java index dab7aee3cf2..2eabc0d521b 100644 --- a/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/R2DBCDatabaseContainerProvider.java +++ b/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/R2DBCDatabaseContainerProvider.java @@ -7,7 +7,7 @@ public interface R2DBCDatabaseContainerProvider { Option REUSABLE_OPTION = Option.valueOf("TC_REUSABLE"); - Option IMAGE_OPTION = Option.valueOf("TC_IMAGE"); + Option IMAGE_TAG_OPTION = Option.valueOf("TC_IMAGE_TAG"); boolean supports(ConnectionFactoryOptions options); From 574df74f73adc52999558c70a0186c2f3f12955e Mon Sep 17 00:00:00 2001 From: Sergei Egorov Date: Sun, 12 Apr 2020 12:57:26 +0200 Subject: [PATCH 11/15] fix `reusesUntilConnectionFactoryIsClosed` --- .../r2dbc/TestcontainersR2DBCConnectionFactoryTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/r2dbc/src/test/java/org/testcontainers/r2dbc/TestcontainersR2DBCConnectionFactoryTest.java b/modules/r2dbc/src/test/java/org/testcontainers/r2dbc/TestcontainersR2DBCConnectionFactoryTest.java index b75f465336e..6fad93f0e2a 100644 --- a/modules/r2dbc/src/test/java/org/testcontainers/r2dbc/TestcontainersR2DBCConnectionFactoryTest.java +++ b/modules/r2dbc/src/test/java/org/testcontainers/r2dbc/TestcontainersR2DBCConnectionFactoryTest.java @@ -30,7 +30,7 @@ public void failsOnUnknownProvider() { @Test public void reusesUntilConnectionFactoryIsClosed() { - String url = "r2dbc:tc:postgresql:///db?TC_IMAGE=postgres:10-alpine"; + String url = "r2dbc:tc:postgresql:///db?TC_IMAGE_TAG=10-alpine"; ConnectionFactory connectionFactory = ConnectionFactories.get(url); Integer updated = Flux From e145cc1b4bd088b9bc87ba562206938587133715 Mon Sep 17 00:00:00 2001 From: Sergei Egorov Date: Sun, 12 Apr 2020 13:33:51 +0200 Subject: [PATCH 12/15] add docs --- docs/modules/databases/index.md | 113 +---------------- docs/modules/databases/jdbc.md | 119 ++++++++++++++++++ docs/modules/databases/r2dbc.md | 55 ++++++++ mkdocs.yml | 2 + .../PostgreSQLR2DBCDatabaseContainerTest.java | 8 +- 5 files changed, 184 insertions(+), 113 deletions(-) create mode 100644 docs/modules/databases/jdbc.md create mode 100644 docs/modules/databases/r2dbc.md diff --git a/docs/modules/databases/index.md b/docs/modules/databases/index.md index d7654cc6a33..6685ade55c8 100644 --- a/docs/modules/databases/index.md +++ b/docs/modules/databases/index.md @@ -10,115 +10,4 @@ You might want to use Testcontainers' database support: !!! note Of course, it's still important to have as few tests that hit the database as possible, and make good use of mocks for components higher up the stack. -You can obtain a temporary database in one of two ways: - - * **JUnit @Rule/@ClassRule**: this mode starts a database inside a container before your tests and tears it down afterwards. - * **Using a specially modified JDBC URL**: after making a very simple modification to your system's JDBC URL string, Testcontainers will provide a disposable stand-in database that can be used without requiring modification to your application code. - -## Database container objects - -Add a @Rule or @ClassRule to your test class, e.g.: - -```java -public class SimpleMySQLTest { - @Rule - public MySQLContainer mysql = new MySQLContainer(); -``` - -Now, in your test code (or a suitable setup method), you can obtain details necessary to connect to this database: - - * `mysql.getJdbcUrl()` provides a JDBC URL your code can connect to - * `mysql.getUsername()` provides the username your code should pass to the driver - * `mysql.getPassword()` provides the password your code should pass to the driver - -Note that if you use `@Rule`, you will be given an isolated container for each test method. If you use `@ClassRule`, you will get on isolated container for all the methods in the test class. - -Examples/Tests: - - * [MySQL](https://github.com/testcontainers/testcontainers-java/blob/master/modules/jdbc-test/src/test/java/org/testcontainers/junit/SimpleMySQLTest.java) - * [PostgreSQL](https://github.com/testcontainers/testcontainers-java/blob/master/modules/jdbc-test/src/test/java/org/testcontainers/junit/SimplePostgreSQLTest.java) - -## Database containers launched via JDBC URL scheme - -As long as you have Testcontainers and the appropriate JDBC driver on your classpath, you can simply modify regular JDBC connection URLs to get a fresh containerized instance of the database each time your application starts up. - -_N.B:_ - -* _TC needs to be on your application's classpath at runtime for this to work_ -* _For Spring Boot (Before version `2.3.0`) you need to specify the driver manually `spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver`_ - -**Original URL**: `jdbc:mysql:5.7.22://localhost:3306/databasename` - -Insert `tc:` after `jdbc:` as follows. Note that the hostname, port and database name will be ignored; you can leave these as-is or set them to any value. - -!!! note - We will use `///` (host-less URIs) from now on to emphasis the unimportance of the `host:port` pair. - From Testcontainers' perspective, `jdbc:mysql:5.7.22://localhost:3306/databasename` and `jdbc:mysql:5.7.22:///databasename` is the same URI. - -### JDBC URL examples - -#### Using Testcontainers with a fixed version - -`jdbc:tc:mysql:5.6.23:///databasename` - -#### Using PostgreSQL - -`jdbc:tc:postgresql:9.6.8:///databasename` - -### Using PostGIS - -`jdbc:tc:postgis:9.6:///databasename` - -### Using Presto - -`jdbc:tc:presto:329://localhost/memory/default` - -## Using a classpath init script - -Testcontainers can run an init script after the database container is started, but before your code is given a connection to it. The script must be on the classpath, and is referenced as follows: - -`jdbc:tc:mysql:5.7.22:///databasename?TC_INITSCRIPT=somepath/init_mysql.sql` - -This is useful if you have a fixed script for setting up database schema, etc. - -## Using an init script from a file - -If the init script path is prefixed `file:`, it will be loaded from a file (relative to the working directory, which will usually be the project root). - -`jdbc:tc:mysql:5.7.22:///databasename?TC_INITSCRIPT=file:src/main/resources/init_mysql.sql` - -#### Using an init function - -Instead of running a fixed script for DB setup, it may be useful to call a Java function that you define. This is intended to allow you to trigger database schema migration tools. To do this, add TC_INITFUNCTION to the URL as follows, passing a full path to the class name and method: - - `jdbc:tc:mysql:5.7.22:///databasename?TC_INITFUNCTION=org.testcontainers.jdbc.JDBCDriverTest::sampleInitFunction` - -The init function must be a public static method which takes a `java.sql.Connection` as its only parameter, e.g. -```java -public class JDBCDriverTest { - public static void sampleInitFunction(Connection connection) throws SQLException { - // e.g. run schema setup or Flyway/liquibase/etc DB migrations here... - } - ... -``` - -#### Running container in daemon mode - -By default database container is being stopped as soon as last connection is closed. There are cases when you might need to start container and keep it running till you stop it explicitly or JVM is shutdown. To do this, add `TC_DAEMON` parameter to the URL as follows: - - `jdbc:tc:mysql:5.7.22:///databasename?TC_DAEMON=true` - -With this parameter database container will keep running even when there're no open connections. - - -#### Running container with tmpfs options - -Container can have `tmpfs` mounts for storing data in host memory. This is useful if you want to speed up your database tests. Be aware that the data will be lost when the container stops. - -To pass this option to the container, add `TC_TMPFS` parameter to the URL as follows: - - `jdbc:tc:postgresql:9.6.8:///databasename?TC_TMPFS=/testtmpfs:rw` - -If you need more than one option, separate them by comma (e.g. `TC_TMPFS=key:value,key1:value1&other_parameters=foo`). - -For more information about `tmpfs` mount, see [the official Docker documentation](https://docs.docker.com/storage/tmpfs/). +See [JDBC](./jdbc.md) and [R2DBC](./r2dbc.md) for information on how to use Testcontainers with SQL-like databases. \ No newline at end of file diff --git a/docs/modules/databases/jdbc.md b/docs/modules/databases/jdbc.md new file mode 100644 index 00000000000..5f83d7fcdeb --- /dev/null +++ b/docs/modules/databases/jdbc.md @@ -0,0 +1,119 @@ +# JDBC support + +You can obtain a temporary database in one of two ways: + + * **Using a specially modified JDBC URL**: after making a very simple modification to your system's JDBC URL string, Testcontainers will provide a disposable stand-in database that can be used without requiring modification to your application code. + * **JUnit @Rule/@ClassRule**: this mode starts a database inside a container before your tests and tears it down afterwards. + +## Database containers launched via JDBC URL scheme + +As long as you have Testcontainers and the appropriate JDBC driver on your classpath, you can simply modify regular JDBC connection URLs to get a fresh containerized instance of the database each time your application starts up. + +_N.B:_ + +* _TC needs to be on your application's classpath at runtime for this to work_ +* _For Spring Boot (Before version `2.3.0`) you need to specify the driver manually `spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver`_ + +**Original URL**: `jdbc:mysql://localhost:3306/databasename` + +Insert `tc:` after `jdbc:` as follows. Note that the hostname, port and database name will be ignored; you can leave these as-is or set them to any value. + +!!! note + We will use `///` (host-less URIs) from now on to emphasis the unimportance of the `host:port` pair. + From Testcontainers' perspective, `jdbc:mysql:5.7.22://localhost:3306/databasename` and `jdbc:mysql:5.7.22:///databasename` is the same URI. + +!!! warning + If you're using the JDBC URL support, there is no need to instantiate an instance of the container - Testcontainers will do it automagically. + +### JDBC URL examples + +#### Using Testcontainers with a fixed version + +`jdbc:tc:mysql:5.6.23:///databasename` + +#### Using PostgreSQL + +`jdbc:tc:postgresql:9.6.8:///databasename` + +#### Using PostGIS + +`jdbc:tc:postgis:9.6:///databasename` + +#### Using Presto + +`jdbc:tc:presto:329://localhost/memory/default` + +### Using a classpath init script + +Testcontainers can run an init script after the database container is started, but before your code is given a connection to it. The script must be on the classpath, and is referenced as follows: + +`jdbc:tc:mysql:5.7.22:///databasename?TC_INITSCRIPT=somepath/init_mysql.sql` + +This is useful if you have a fixed script for setting up database schema, etc. + +### Using an init script from a file + +If the init script path is prefixed `file:`, it will be loaded from a file (relative to the working directory, which will usually be the project root). + +`jdbc:tc:mysql:5.7.22:///databasename?TC_INITSCRIPT=file:src/main/resources/init_mysql.sql` + +### Using an init function + +Instead of running a fixed script for DB setup, it may be useful to call a Java function that you define. This is intended to allow you to trigger database schema migration tools. To do this, add TC_INITFUNCTION to the URL as follows, passing a full path to the class name and method: + + `jdbc:tc:mysql:5.7.22:///databasename?TC_INITFUNCTION=org.testcontainers.jdbc.JDBCDriverTest::sampleInitFunction` + +The init function must be a public static method which takes a `java.sql.Connection` as its only parameter, e.g. +```java +public class JDBCDriverTest { + public static void sampleInitFunction(Connection connection) throws SQLException { + // e.g. run schema setup or Flyway/liquibase/etc DB migrations here... + } + ... +``` + +### Running container in daemon mode + +By default database container is being stopped as soon as last connection is closed. There are cases when you might need to start container and keep it running till you stop it explicitly or JVM is shutdown. To do this, add `TC_DAEMON` parameter to the URL as follows: + + `jdbc:tc:mysql:5.7.22:///databasename?TC_DAEMON=true` + +With this parameter database container will keep running even when there're no open connections. + + +### Running container with tmpfs options + +Container can have `tmpfs` mounts for storing data in host memory. This is useful if you want to speed up your database tests. Be aware that the data will be lost when the container stops. + +To pass this option to the container, add `TC_TMPFS` parameter to the URL as follows: + + `jdbc:tc:postgresql:9.6.8:///databasename?TC_TMPFS=/testtmpfs:rw` + +If you need more than one option, separate them by comma (e.g. `TC_TMPFS=key:value,key1:value1&other_parameters=foo`). + +For more information about `tmpfs` mount, see [the official Docker documentation](https://docs.docker.com/storage/tmpfs/). + +## Database container objects + +In case you can't use the URL support, or need to fine-tune the container, you can instantiate it yourself. + +Add a @Rule or @ClassRule to your test class, e.g.: + +```java +public class SimpleMySQLTest { + @Rule + public MySQLContainer mysql = new MySQLContainer(); +``` + +Now, in your test code (or a suitable setup method), you can obtain details necessary to connect to this database: + + * `mysql.getJdbcUrl()` provides a JDBC URL your code can connect to + * `mysql.getUsername()` provides the username your code should pass to the driver + * `mysql.getPassword()` provides the password your code should pass to the driver + +Note that if you use `@Rule`, you will be given an isolated container for each test method. If you use `@ClassRule`, you will get on isolated container for all the methods in the test class. + +Examples/Tests: + + * [MySQL](https://github.com/testcontainers/testcontainers-java/blob/master/modules/jdbc-test/src/test/java/org/testcontainers/junit/SimpleMySQLTest.java) + * [PostgreSQL](https://github.com/testcontainers/testcontainers-java/blob/master/modules/jdbc-test/src/test/java/org/testcontainers/junit/SimplePostgreSQLTest.java) \ No newline at end of file diff --git a/docs/modules/databases/r2dbc.md b/docs/modules/databases/r2dbc.md new file mode 100644 index 00000000000..5e4393b95a5 --- /dev/null +++ b/docs/modules/databases/r2dbc.md @@ -0,0 +1,55 @@ +# R2DBC support + +You can obtain a temporary database in one of two ways: + + * **Using a specially modified R2DBC URL**: after making a very simple modification to your system's R2DBC URL string, Testcontainers will provide a disposable stand-in database that can be used without requiring modification to your application code. + * **JUnit @Rule/@ClassRule**: this mode starts a database inside a container before your tests and tears it down afterwards. + +## Database containers launched via R2DBC URL scheme + +As long as you have Testcontainers and the appropriate R2DBC driver on your classpath, you can simply modify regular R2DBC connection URLs to get a fresh containerized instance of the database each time your application starts up. + +Started container will be terminated when the `ConnectionFactory` is closed. + +!!! warning + both the database module like `org.testcontainers:mysql` **and** `org.testcontainers:r2dbc` need to be on your application's classpath at runtime for this to work. + +**Original URL**: `r2dbc:mysql://localhost:3306/databasename` + +1. Insert `tc:` after `r2dbc:` as follows. Note that the hostname, port and database name will be ignored; you can leave these as-is or set them to any value. +1. Specify the mandatory Docker tag of database's official image that you want to use as `TC_IMAGE_TAG` + +So that the URL becomes: +`r2dbc:tc:mysql:///databasename?TC_IMAGE_TAG=5.7.22` + +!!! note + We will use `///` (host-less URIs) from now on to emphasis the unimportance of the `host:port` pair. + From Testcontainers' perspective, `r2dbc:mysql://localhost:3306/databasename` and `r2dbc:mysql:///databasename` is the same URI. + +!!! warning + If you're using the R2DBC URL support, there is no need to instantiate an instance of the container - Testcontainers will do it automagically. + +### R2DBC URL examples + +#### Using MySQL + +`r2dbc:tc:mysql:///databasename?TC_IMAGE_TAG=5.6.23` + +#### Using MariaDB + +`r2dbc:tc:mariadb:///databasename?TC_IMAGE_TAG=10.3.6` + +#### Using PostgreSQL + +`r2dbc:tc:postgresql:///databasename?TC_IMAGE_TAG=9.6.8` + +#### Using MSSQL: + +`r2dbc:tc:sqlserver:///?TC_IMAGE_TAG=2017-CU12` + +## Obtaining `ConnectionFactoryOptions` from database container objects + +If you already have an instance of the database container, you can get an instance of `ConnectionFactoryOptions` from it: + +[Creating `ConnectionFactoryOptions` from an instance)](../../../modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerTest.java) inside_block:get_options + diff --git a/mkdocs.yml b/mkdocs.yml index 49876885dfd..c2d39bc9d2c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -40,6 +40,8 @@ nav: - Modules: - Databases: - modules/databases/index.md + - modules/databases/jdbc.md + - modules/databases/r2dbc.md - modules/databases/cassandra.md - modules/databases/cockroachdb.md - modules/databases/couchbase.md diff --git a/modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerTest.java b/modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerTest.java index ab692ef5275..505b37859ba 100644 --- a/modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerTest.java +++ b/modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerTest.java @@ -12,7 +12,13 @@ protected PostgreSQLContainer createContainer() { @Override protected ConnectionFactoryOptions getOptions(PostgreSQLContainer container) { - return PostgreSQLR2DBCDatabaseContainer.getOptions(container); + // get_options { + ConnectionFactoryOptions options = PostgreSQLR2DBCDatabaseContainer.getOptions( + container + ); + // } + + return options; } protected String createR2DBCUrl() { From 830f483fc8f7aff05bb3b7e6a5831a89e25fdbc6 Mon Sep 17 00:00:00 2001 From: Sergei Egorov Date: Sun, 12 Apr 2020 15:38:05 +0200 Subject: [PATCH 13/15] Update docs/modules/databases/r2dbc.md Co-Authored-By: Richard North --- docs/modules/databases/r2dbc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/databases/r2dbc.md b/docs/modules/databases/r2dbc.md index 5e4393b95a5..f4670765f3f 100644 --- a/docs/modules/databases/r2dbc.md +++ b/docs/modules/databases/r2dbc.md @@ -12,7 +12,7 @@ As long as you have Testcontainers and the appropriate R2DBC driver on your clas Started container will be terminated when the `ConnectionFactory` is closed. !!! warning - both the database module like `org.testcontainers:mysql` **and** `org.testcontainers:r2dbc` need to be on your application's classpath at runtime for this to work. + Both the database module (e.g. `org.testcontainers:mysql`) **and** `org.testcontainers:r2dbc` need to be on your application's classpath at runtime. **Original URL**: `r2dbc:mysql://localhost:3306/databasename` From 3db13dc3c22f1a0a009a1552704abda074ec498f Mon Sep 17 00:00:00 2001 From: Sergei Egorov Date: Sun, 12 Apr 2020 15:39:09 +0200 Subject: [PATCH 14/15] Update docs/modules/databases/r2dbc.md Co-Authored-By: Richard North --- docs/modules/databases/r2dbc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/databases/r2dbc.md b/docs/modules/databases/r2dbc.md index f4670765f3f..8404f69e7e7 100644 --- a/docs/modules/databases/r2dbc.md +++ b/docs/modules/databases/r2dbc.md @@ -9,7 +9,7 @@ You can obtain a temporary database in one of two ways: As long as you have Testcontainers and the appropriate R2DBC driver on your classpath, you can simply modify regular R2DBC connection URLs to get a fresh containerized instance of the database each time your application starts up. -Started container will be terminated when the `ConnectionFactory` is closed. +The started container will be terminated when the `ConnectionFactory` is closed. !!! warning Both the database module (e.g. `org.testcontainers:mysql`) **and** `org.testcontainers:r2dbc` need to be on your application's classpath at runtime. From 38e81dc1c6a23a90432cb9b2da50c07e2f609e80 Mon Sep 17 00:00:00 2001 From: Sergei Egorov Date: Sun, 12 Apr 2020 16:20:48 +0200 Subject: [PATCH 15/15] Update docs/modules/databases/r2dbc.md Co-Authored-By: Richard North --- docs/modules/databases/r2dbc.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/modules/databases/r2dbc.md b/docs/modules/databases/r2dbc.md index 8404f69e7e7..6ba3f401a08 100644 --- a/docs/modules/databases/r2dbc.md +++ b/docs/modules/databases/r2dbc.md @@ -17,7 +17,9 @@ The started container will be terminated when the `ConnectionFactory` is closed. **Original URL**: `r2dbc:mysql://localhost:3306/databasename` 1. Insert `tc:` after `r2dbc:` as follows. Note that the hostname, port and database name will be ignored; you can leave these as-is or set them to any value. -1. Specify the mandatory Docker tag of database's official image that you want to use as `TC_IMAGE_TAG` +1. Specify the mandatory Docker tag of the database's official image that you want using a `TC_IMAGE_TAG` query parameter. + +**Note that, unlike Testcontainers' JDBC URL support, it is not possible to specify an image tag in the 'scheme' part of the URL, and it is always necessary to specify a tag using `TC_IMAGE_TAG`.** So that the URL becomes: `r2dbc:tc:mysql:///databasename?TC_IMAGE_TAG=5.7.22`