Migrate Cassandra examples to JUnit 5.

See #583
This commit is contained in:
Mark Paluch
2021-03-23 08:50:52 +01:00
parent 4bcf378114
commit 60385ee4b4
25 changed files with 568 additions and 534 deletions

View File

@@ -1,86 +0,0 @@
/*
* Copyright 2017-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.springdata.cassandra.util;
import java.util.concurrent.TimeUnit;
import org.cassandraunit.utils.EmbeddedCassandraServerHelper;
import org.junit.AssumptionViolatedException;
/**
* {@link org.junit.rules.TestRule} for Cassandra server use. This rule can start a Cassandra instance, reuse a running
* instance or simply require a running Cassandra server (will skip the test if Cassandra is not running).
*
* @author Mark Paluch
*/
public class Cassandra extends CassandraResource {
private final RuntimeMode runtimeMode;
private Cassandra(String host, int port, RuntimeMode runtimeMode) {
super(host, port);
this.runtimeMode = runtimeMode;
}
/**
* Require a running instance on {@code host:port}. Fails with {@link AssumptionViolatedException} if Cassandra is not
* running.
*
* @param host must not be {@literal null} or empty.
* @param port must be between 0 and 65535.
* @return the {@link Cassandra} rule
*/
public static Cassandra requireRunningInstance(String host, int port) {
return new Cassandra(host, port, RuntimeMode.REQUIRE_RUNNING_INSTANCE);
}
/**
* Start an embedded Cassandra instance on {@code host:port} if Cassandra is not running already.
*
* @param host must not be {@literal null} or empty.
* @param port must be between 0 and 65535.
* @return the {@link Cassandra} rule
*/
public static Cassandra embeddedIfNotRunning(String host, int port) {
return new Cassandra(host, port, RuntimeMode.EMBEDDED_IF_NOT_RUNNING);
}
@Override
protected void before() throws Throwable {
if (runtimeMode == RuntimeMode.REQUIRE_RUNNING_INSTANCE) {
if (!CassandraSocket.isConnectable(getHost(), getPort())) {
throw new AssumptionViolatedException(
String.format("Cassandra is not reachable at %s:%s.", getHost(), getPort()));
}
}
if (runtimeMode == RuntimeMode.EMBEDDED_IF_NOT_RUNNING) {
if (CassandraSocket.isConnectable(getHost(), getPort())) {
return;
}
}
EmbeddedCassandraServerHelper.startEmbeddedCassandra("embedded-cassandra.yaml", "target/embeddedCassandra",
TimeUnit.SECONDS.toMillis(60));
super.before();
}
private enum RuntimeMode {
REQUIRE_RUNNING_INSTANCE, EMBEDDED_IF_NOT_RUNNING;
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.springdata.cassandra.util;
import java.net.InetSocketAddress;
import java.util.Optional;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.platform.commons.util.AnnotationUtils;
import com.datastax.oss.driver.api.core.CqlSession;
/**
* JUnit 5 {@link BeforeAllCallback} extension to ensure a running Cassandra server.
*
* @author Mark Paluch
* @see CassandraKeyspace
*/
class CassandraExtension implements BeforeAllCallback {
private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace
.create(CassandraExtension.class);
@Override
public void beforeAll(ExtensionContext context) throws Exception {
ExtensionContext.Store store = context.getStore(NAMESPACE);
CassandraKeyspace cassandra = findAnnotation(context);
CassandraServer keyspace = store.getOrComputeIfAbsent(CassandraServer.class, it -> {
return CassandraServer.embeddedIfNotRunning("localhost", 9042);
}, CassandraServer.class);
keyspace.before();
CqlSession session = store.getOrComputeIfAbsent(CqlSession.class, it -> {
return CqlSession.builder().addContactPoint(new InetSocketAddress("localhost", 9042))
.withLocalDatacenter("datacenter1").build();
}, CqlSession.class);
session.execute(String.format("CREATE KEYSPACE IF NOT EXISTS %s \n"
+ "WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };", cassandra.keyspace()));
}
private static CassandraKeyspace findAnnotation(ExtensionContext context) {
Class<?> testClass = context.getRequiredTestClass();
Optional<CassandraKeyspace> annotation = AnnotationUtils.findAnnotation(testClass, CassandraKeyspace.class);
return annotation.orElseThrow(() -> new IllegalStateException("Test class not annotated with @Cassandra"));
}
}

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2016-2018 the original author or authors.
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -15,111 +15,29 @@
*/
package example.springdata.cassandra.util;
import java.net.InetSocketAddress;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.AssumptionViolatedException;
import org.springframework.data.util.Version;
import org.springframework.util.Assert;
import com.datastax.oss.driver.api.core.CqlSession;
import org.junit.jupiter.api.extension.ExtendWith;
/**
* {@link CassandraResource} to require (create or reuse) an Apache Cassandra keyspace and optionally require a specific
* Apache Cassandra version. This {@link org.junit.rules.TestRule} can be chained to depend on another
* {@link CassandraResource} rule to require a running instance/start an embedded Apache Cassandra instance.
* Annotation that can activates embedded Cassandra providing a keyspace at {@link #keyspace()}
*
* @author Mark Paluch
*/
public class CassandraKeyspace extends CassandraResource {
private final String keyspaceName;
private final Version requiredVersion;
private final CassandraResource dependency;
private CassandraKeyspace(String host, int port, String keyspaceName, CassandraResource dependency,
Version requiredVersion) {
super(host, port);
this.keyspaceName = keyspaceName;
this.dependency = dependency;
this.requiredVersion = requiredVersion;
}
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@ExtendWith(CassandraExtension.class)
public @interface CassandraKeyspace {
/**
* Create a {@link CassandraKeyspace} test rule to provide a running Cassandra instance on {@code localhost:9042} with
* a keyspace {@code example}. Reuses a running Cassandra instance if available or starts an embedded instance.
* Name of the desired keyspace to be provided.
*
* @return the {@link CassandraKeyspace} rule.
* @return
*/
public static CassandraKeyspace onLocalhost() {
return new CassandraKeyspace("localhost", 9042, "example", Cassandra.embeddedIfNotRunning("localhost", 9042),
new Version(0, 0, 0));
}
/**
* Setup a dependency to an upstream {@link CassandraResource}. The dependency is activated by {@code this} test rule.
*
* @param cassandraResource must not be {@literal null}.
* @return the {@link CassandraKeyspace} rule.
*/
public CassandraKeyspace dependsOn(CassandraResource cassandraResource) {
Assert.notNull(cassandraResource, "CassandraResource must not be null!");
return new CassandraKeyspace(getHost(), getPort(), keyspaceName, cassandraResource, requiredVersion);
}
/**
* Setup a version requirement.
*
* @param requiredVersion must not be {@literal null}.
* @return the {@link CassandraKeyspace} rule
*/
public CassandraKeyspace atLeast(Version requiredVersion) {
Assert.notNull(requiredVersion, "Required version must not be null!");
return new CassandraKeyspace(getHost(), getPort(), keyspaceName, dependency, requiredVersion);
}
/*
* (non-Javadoc)
* @see org.junit.rules.ExternalResource#before()
*/
@Override
protected void before() throws Throwable {
dependency.before();
try (CqlSession session = CqlSession.builder().addContactPoint(new InetSocketAddress(getHost(), getPort()))
.withLocalDatacenter("datacenter1").build()) {
if (requiredVersion != null) {
Version cassandraReleaseVersion = CassandraVersion.getReleaseVersion(session);
if (cassandraReleaseVersion.isLessThan(requiredVersion)) {
throw new AssumptionViolatedException(
String.format("Cassandra at %s:%s runs in Version %s but we require at least %s", getHost(), getPort(),
cassandraReleaseVersion, requiredVersion));
}
}
session.execute(String.format("CREATE KEYSPACE IF NOT EXISTS %s \n"
+ "WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };", keyspaceName));
}
}
/*
* (non-Javadoc)
* @see org.junit.rules.ExternalResource#after()
*/
@Override
protected void after() {
super.after();
dependency.after();
}
String keyspace() default "example";
}

View File

@@ -1,71 +0,0 @@
/*
* Copyright 2017-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.springdata.cassandra.util;
import org.junit.rules.ExternalResource;
import org.springframework.util.Assert;
/**
* Base class to abstract contact point details for Apache Cassandra as {@link ExternalResource}.
*
* @author Mark Paluch
*/
public abstract class CassandraResource extends ExternalResource {
private final String host;
private final int port;
CassandraResource(String host, int port) {
Assert.hasText(host, "Host must not be null or empty!");
Assert.isTrue(port >= 0 && port <= 65535, "Port must be in the range of 0..65535!");
this.host = host;
this.port = port;
}
/**
* @return the Cassandra hostname.
*/
public String getHost() {
return host;
}
/**
* @return the Cassandra port.
*/
public int getPort() {
return port;
}
/*
* (non-Javadoc)
* @see org.junit.rules.ExternalResource#before()
*/
@Override
protected void before() throws Throwable {
super.before();
}
/*
* (non-Javadoc)
* @see org.junit.rules.ExternalResource#after()
*/
@Override
protected void after() {
super.after();
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2017-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.springdata.cassandra.util;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.concurrent.TimeUnit;
import org.cassandraunit.utils.EmbeddedCassandraServerHelper;
import org.junit.jupiter.api.Assumptions;
import org.springframework.util.Assert;
/**
* Utility for Cassandra server use. This utility can start a Cassandra instance, reuse a running instance or simply
* require a running Cassandra server (will skip the test if Cassandra is not running).
*
* @author Mark Paluch
*/
class CassandraServer {
private final String host;
private final int port;
private final RuntimeMode runtimeMode;
private CassandraServer(String host, int port, RuntimeMode runtimeMode) {
this.host = host;
this.port = port;
this.runtimeMode = runtimeMode;
}
/**
* Require a running instance on {@code host:port}. Fails with {@link AssumptionViolatedException} if Cassandra is not
* running.
*
* @param host must not be {@literal null} or empty.
* @param port must be between 0 and 65535.
* @return the {@link CassandraServer} rule
*/
public static CassandraServer requireRunningInstance(String host, int port) {
return new CassandraServer(host, port, RuntimeMode.REQUIRE_RUNNING_INSTANCE);
}
/**
* Start an embedded Cassandra instance on {@code host:port} if Cassandra is not running already.
*
* @param host must not be {@literal null} or empty.
* @param port must be between 0 and 65535.
* @return the {@link CassandraServer} rule
*/
public static CassandraServer embeddedIfNotRunning(String host, int port) {
return new CassandraServer(host, port, RuntimeMode.EMBEDDED_IF_NOT_RUNNING);
}
/**
* @param host must not be {@literal null} or empty.
* @param port
* @return {@literal true} if the TCP port accepts a connection.
*/
public static boolean isConnectable(String host, int port) {
Assert.hasText(host, "Host must not be null or empty!");
try (Socket socket = new Socket()) {
socket.setSoLinger(true, 0);
socket.connect(new InetSocketAddress(host, port), (int) TimeUnit.MILLISECONDS.convert(10, TimeUnit.SECONDS));
return true;
} catch (Exception e) {
return false;
}
}
public String getHost() {
return host;
}
public int getPort() {
return port;
}
protected void before() throws Exception {
if (runtimeMode == RuntimeMode.REQUIRE_RUNNING_INSTANCE) {
Assumptions.assumeTrue(isConnectable(getHost(), getPort()),
() -> String.format("Cassandra is not reachable at %s:%s.", getHost(), getPort()));
}
if (runtimeMode == RuntimeMode.EMBEDDED_IF_NOT_RUNNING) {
if (isConnectable(getHost(), getPort())) {
return;
}
}
EmbeddedCassandraServerHelper.startEmbeddedCassandra("embedded-cassandra.yaml", "target/embeddedCassandra",
TimeUnit.SECONDS.toMillis(60));
}
private enum RuntimeMode {
REQUIRE_RUNNING_INSTANCE, EMBEDDED_IF_NOT_RUNNING;
}
}

View File

@@ -1,52 +0,0 @@
/*
* Copyright 2017-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.springdata.cassandra.util;
import lombok.experimental.UtilityClass;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.concurrent.TimeUnit;
import org.springframework.util.Assert;
/**
* @author Mark Paluch
*/
@UtilityClass
class CassandraSocket {
/**
* @param host must not be {@literal null} or empty.
* @param port
* @return {@literal true} if the TCP port accepts a connection.
*/
public static boolean isConnectable(String host, int port) {
Assert.hasText(host, "Host must not be null or empty!");
try (Socket socket = new Socket()) {
socket.setSoLinger(true, 0);
socket.connect(new InetSocketAddress(host, port), (int) TimeUnit.MILLISECONDS.convert(10, TimeUnit.SECONDS));
return true;
} catch (Exception e) {
return false;
}
}
}

View File

@@ -0,0 +1,234 @@
# Configuration for the DataStax Java driver for Apache Cassandra®.
#
# Unless you use a custom mechanism to load your configuration (see
# SessionBuilder.withConfigLoader), all the values declared here will be used as defaults. You can
# place your own `application.conf` in the classpath to override them.
#
# Options are classified into two categories:
# - basic: what is most likely to be customized first when kickstarting a new application.
# - advanced: more elaborate tuning options, or "expert"-level customizations.
#
# This file is in HOCON format, see https://github.com/typesafehub/config/blob/master/HOCON.md.
datastax-java-driver {
basic.load-balancing-policy {
class = DcInferringLoadBalancingPolicy
}
basic.request {
# How long the driver waits for a request to complete. This is a global limit on the duration of
# a session.execute() call, including any internal retries the driver might do.
#
# By default, this value is set pretty high to ensure that DDL queries don't time out, in order
# to provide the best experience for new users trying the driver with the out-of-the-box
# configuration.
# For any serious deployment, we recommend that you use separate configuration profiles for DDL
# and DML; you can then set the DML timeout much lower (down to a few milliseconds if needed).
#
# Note that, because timeouts are scheduled on the driver's timer thread, the duration specified
# here must be greater than the timer tick duration defined by the
# advanced.netty.timer.tick-duration setting (see below). If that is not the case, timeouts will
# not be triggered as timely as desired.
#
# Required: yes
# Modifiable at runtime: yes, the new value will be used for requests issued after the change.
# Overridable in a profile: yes
timeout = 8 seconds
}
# ADVANCED OPTIONS -------------------------------------------------------------------------------
advanced.connection {
# The timeout to use for internal queries that run as part of the initialization process, just
# after we open a connection. If this timeout fires, the initialization of the connection will
# fail. If this is the first connection ever, the driver will fail to initialize as well,
# otherwise it will retry the connection later.
#
# Required: yes
# Modifiable at runtime: yes, the new value will be used for connections created after the
# change.
# Overridable in a profile: no
init-query-timeout = 500 milliseconds
# The driver maintains a connection pool to each node, according to the distance assigned to it
# by the load balancing policy. If the distance is IGNORED, no connections are maintained.
pool {
local {
# The number of connections in the pool.
#
# Required: yes
# Modifiable at runtime: yes; when the change is detected, all active pools will be notified
# and will adjust their size.
# Overridable in a profile: no
size = 1
}
remote {
size = 1
}
}
}
advanced.metrics {
# The session-level metrics (all disabled by default).
#
# Required: yes
# Modifiable at runtime: no
# Overridable in a profile: no
session {
enabled = []
}
# The node-level metrics (all disabled by default).
#
# Required: yes
# Modifiable at runtime: no
# Overridable in a profile: no
node {
enabled = []
}
}
advanced.control-connection {
schema-agreement {
# The interval between each attempt.
# Required: yes
# Modifiable at runtime: yes, the new value will be used for checks issued after the change.
# Overridable in a profile: no
interval = 100 seconds
# The timeout after which schema agreement fails.
# If this is set to 0, schema agreement is skipped and will always fail.
#
# Required: yes
# Modifiable at runtime: yes, the new value will be used for checks issued after the change.
# Overridable in a profile: no
timeout = 100 seconds
# Whether to log a warning if schema agreement fails.
# You might want to change this if you've set the timeout to 0.
#
# Required: yes
# Modifiable at runtime: yes, the new value will be used for checks issued after the change.
# Overridable in a profile: no
warn-on-failure = true
}
}
# Options related to the Netty event loop groups used internally by the driver.
advanced.netty {
# Whether the threads created by the driver should be daemon threads.
# This will apply to the threads in io-group, admin-group, and the timer thread.
#
# Required: yes
# Modifiable at runtime: no
# Overridable in a profile: no
daemon = false
# The event loop group used for I/O operations (reading and writing to Cassandra nodes).
# By default, threads in this group are named after the session name, "-io-" and an incrementing
# counter, for example "s0-io-0".
io-group {
# The number of threads.
# If this is set to 0, the driver will use `Runtime.getRuntime().availableProcessors() * 2`.
#
# Required: yes
# Modifiable at runtime: no
# Overridable in a profile: no
size = 4
# The options to shut down the event loop group gracefully when the driver closes. If a task
# gets submitted during the quiet period, it is accepted and the quiet period starts over.
# The timeout limits the overall shutdown time.
#
# Required: yes
# Modifiable at runtime: no
# Overridable in a profile: no
shutdown {quiet-period = 0, timeout = 0, unit = SECONDS}
}
# The event loop group used for admin tasks not related to request I/O (handle cluster events,
# refresh metadata, schedule reconnections, etc.)
# By default, threads in this group are named after the session name, "-admin-" and an
# incrementing counter, for example "s0-admin-0".
admin-group {
size = 2
shutdown {quiet-period = 0, timeout = 0, unit = SECONDS}
}
}
advanced.metadata {
# Topology events are external signals that inform the driver of the state of Cassandra nodes
# (by default, they correspond to gossip events received on the control connection).
# The debouncer helps smoothen out oscillations if conflicting events are sent out in short
# bursts.
# Debouncing may be disabled by setting the window to 0 or max-events to 1 (this is not
# recommended).
topology-event-debouncer {
# How long the driver waits to propagate an event. If another event is received within that
# time, the window is reset and a batch of accumulated events will be delivered.
#
# Required: yes
# Modifiable at runtime: no
# Overridable in a profile: no
window = 0 second
# The maximum number of events that can accumulate. If this count is reached, the events are
# delivered immediately and the time window is reset. This avoids holding events indefinitely
# if the window keeps getting reset.
#
# Required: yes
# Modifiable at runtime: no
# Overridable in a profile: no
max-events = 20
}
# Options relating to schema metadata (Cluster.getMetadata.getKeyspaces).
# This metadata is exposed by the driver for informational purposes, and is also necessary for
# token-aware routing.
schema {
# Whether schema metadata is enabled.
# If this is false, the schema will remain empty, or to the last known value.
#
# Required: yes
# Modifiable at runtime: yes, the new value will be used for refreshes issued after the
# change. It can also be overridden programmatically via Cluster.setSchemaMetadataEnabled.
# Overridable in a profile: no
enabled = true
# Protects against bursts of schema updates (for example when a client issues a sequence of
# DDL queries), by coalescing them into a single update.
# Debouncing may be disabled by setting the window to 0 or max-events to 1 (this is highly
# discouraged for schema refreshes).
debouncer {
# How long the driver waits to apply a refresh. If another refresh is requested within that
# time, the window is reset and a single refresh will be triggered when it ends.
#
# Required: yes
# Modifiable at runtime: no
# Overridable in a profile: no
window = 0 second
# The maximum number of refreshes that can accumulate. If this count is reached, a refresh
# is done immediately and the window is reset.
#
# Required: yes
# Modifiable at runtime: no
# Overridable in a profile: no
max-events = 20
}
}
# Whether token metadata (Cluster.getMetadata.getTokenMap) is enabled.
# This metadata is exposed by the driver for informational purposes, and is also necessary for
# token-aware routing.
# If this is false, it will remain empty, or to the last known value. Note that its computation
# requires information about the schema; therefore if schema metadata is disabled or filtered to
# a subset of keyspaces, the token map will be incomplete, regardless of the value of this
# property.
#
# Required: yes
# Modifiable at runtime: yes, the new value will be used for refreshes issued after the change.
# Overridable in a profile: no
token-map.enabled = true
}
}