From 8fb80414457a4a780ab0737f0a44f41165744219 Mon Sep 17 00:00:00 2001 From: John Blum Date: Fri, 9 Apr 2021 16:01:20 -0700 Subject: [PATCH] Refactor test framework infrastructure classes. Resolves gh-1115. --- .../CassandraConnectionProperties.java | 85 +++++++++---------- .../test/util/CassandraDelegate.java | 50 ++++++----- .../test/util/CassandraExtension.java | 57 +++++++------ .../util/EmbeddedCassandraServerHelper.java | 18 ++-- 4 files changed, 110 insertions(+), 100 deletions(-) diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/CassandraConnectionProperties.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/CassandraConnectionProperties.java index 75508835a..7ae0a9b6b 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/CassandraConnectionProperties.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/support/CassandraConnectionProperties.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.Properties; import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.NonNull; import org.springframework.util.Assert; /** @@ -31,79 +32,75 @@ import org.springframework.util.Assert; * the build and can be override using system properties. * * @author Mark Paluch + * @author John Blum */ -@SuppressWarnings("serial") +@SuppressWarnings("unused") public class CassandraConnectionProperties extends Properties { private final static List> instances = new ArrayList<>(); - private String resourceName; + private final String resourceName; /** - * Create a new {@link CassandraConnectionProperties} using properties from - * {@code config/cassandra-connection.properties}. + * Construct a new instance of {@link CassandraConnectionProperties} using properties + * from {@code config/cassandra-connection.properties}. */ public CassandraConnectionProperties() { this("/config/cassandra-connection.properties"); } + private CassandraConnectionProperties(@NonNull String resourceName) { + + this.resourceName = resourceName; + + loadProperties(); + + instances.add(new WeakReference<>(this)); + } + public void update() { + try { // Caution: Rewriting properties during initialization. - File file = new File(getClass().getResource(resourceName).toURI()); + File file = new File(getClass().getResource(this.resourceName).toURI()); - try (FileOutputStream fos = new FileOutputStream(file)) { - store(fos, ""); + try (FileOutputStream out = new FileOutputStream(file)) { + store(out, ""); } reload(); - } catch (Exception e) { - e.printStackTrace(); - throw new IllegalStateException(e); + } + catch (Exception cause) { + cause.printStackTrace(); + throw new IllegalStateException(cause); } } private static void reload() { + for (WeakReference ref : instances) { CassandraConnectionProperties properties = ref.get(); + if (properties != null) { properties.loadProperties(); } } } - private CassandraConnectionProperties(String resourceName) { - - this.resourceName = resourceName; - loadProperties(); - - instances.add(new WeakReference<>(this)); - } - private void loadProperties() { - loadProperties(this.resourceName); } private void loadProperties(String resourceName) { - InputStream in = null; - - try { - in = getClass().getResourceAsStream(resourceName); - + try (InputStream in = getClass().getResourceAsStream(resourceName)){ if (in != null) { load(in); } - } catch (Exception cause) { + } + catch (Exception cause) { throw new RuntimeException(cause); - } finally { - if (in != null) { - try { - in.close(); - } catch (Exception ignore) {} - } } } @@ -111,6 +108,7 @@ public class CassandraConnectionProperties extends Properties { public String getProperty(String key) { String value = super.getProperty(key); + if (value == null) { value = System.getProperty(key); } @@ -153,13 +151,6 @@ public class CassandraConnectionProperties extends Properties { return getInt("build.cassandra.rpc_port"); } - /** - * @return the Cassandra Storage port - */ - public int getCassandraStoragePort() { - return getInt("build.cassandra.storage_port"); - } - /** * @return the Cassandra SSL Storage port */ @@ -167,6 +158,13 @@ public class CassandraConnectionProperties extends Properties { return getInt("build.cassandra.ssl_storage_port"); } + /** + * @return the Cassandra Storage port + */ + public int getCassandraStoragePort() { + return getInt("build.cassandra.storage_port"); + } + /** * @return the Cassandra type (Embedded or External) */ @@ -174,12 +172,9 @@ public class CassandraConnectionProperties extends Properties { String cassandraType = getProperty("build.cassandra.mode"); - if (CassandraType.TESTCONTAINERS.name().equalsIgnoreCase(cassandraType)) { - return CassandraType.TESTCONTAINERS; - } - - return CassandraType.EXTERNAL.name().equalsIgnoreCase(cassandraType) ? CassandraType.EXTERNAL - : CassandraType.EMBEDDED; + return CassandraType.TESTCONTAINERS.name().equalsIgnoreCase(cassandraType) ? CassandraType.TESTCONTAINERS + : CassandraType.EXTERNAL.name().equalsIgnoreCase(cassandraType) ? CassandraType.EXTERNAL + : CassandraType.EMBEDDED; } /** @@ -229,6 +224,6 @@ public class CassandraConnectionProperties extends Properties { } public enum CassandraType { - EMBEDDED, EXTERNAL, TESTCONTAINERS; + EMBEDDED, EXTERNAL, TESTCONTAINERS } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/CassandraDelegate.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/CassandraDelegate.java index 54095be49..714761d1e 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/CassandraDelegate.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/CassandraDelegate.java @@ -22,15 +22,16 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import org.testcontainers.containers.CassandraContainer; + import org.springframework.data.cassandra.core.cql.SessionCallback; import org.springframework.data.cassandra.support.CassandraConnectionProperties; import org.springframework.data.cassandra.support.CqlDataSet; +import org.springframework.lang.NonNull; import org.springframework.util.Assert; import org.springframework.util.SocketUtils; import org.springframework.util.StringUtils; -import org.testcontainers.containers.CassandraContainer; - import com.datastax.oss.driver.api.core.CqlIdentifier; import com.datastax.oss.driver.api.core.CqlSession; import com.datastax.oss.driver.api.core.CqlSessionBuilder; @@ -48,22 +49,25 @@ import com.datastax.oss.driver.api.core.metadata.Node; * @author Mark Paluch * @author John Blum * @author Tomasz Lelek + * @see org.springframework.data.cassandra.support.CassandraConnectionProperties + * @see com.datastax.oss.driver.api.core.CqlSessionBuilder + * @see com.datastax.oss.driver.api.core.CqlSession * @since 1.5 - * @see CassandraConnectionProperties */ class CassandraDelegate { - private static ResourceHolder resourceHolder; - private static CassandraContainer container; + private static ResourceHolder resourceHolder; + private final long startupTimeout; - @SuppressWarnings("all") private final CassandraConnectionProperties properties = new CassandraConnectionProperties(); - - private CqlSession system; + private final CassandraConnectionProperties properties = new CassandraConnectionProperties(); private CqlSession session; + private CqlSession system; + + private CqlSessionBuilder sessionBuilder; private Integer cassandraPort; @@ -72,16 +76,16 @@ class CassandraDelegate { private final Map, InvocationMode> invocationModeMap = new HashMap<>(); - private CqlSessionBuilder sessionBuilder; - private final String configurationFilename; /** - * Create a new {@link CassandraDelegate} and allows the use of a config file. + * Create a new {@link CassandraDelegate} allowing the use of a config file. * - * @param yamlConfigurationResource name of the configuration resource, must not be {@literal null} and not empty + * @param yamlConfigurationResource {@link String name} of the configuration resource; + * must not be {@literal null} or {@literal empty}. + * @see #CassandraDelegate(String, long) */ - public CassandraDelegate(String yamlConfigurationResource) { + public CassandraDelegate(@NonNull String yamlConfigurationResource) { this(yamlConfigurationResource, EmbeddedCassandraServerHelper.DEFAULT_STARTUP_TIMEOUT_MS); } @@ -232,6 +236,7 @@ class CassandraDelegate { private void startCassandraIfNeeded() throws Exception { if (isStartNeeded()) { + configureRemoteJmxPort(); if (isEmbedded()) { @@ -263,12 +268,9 @@ class CassandraDelegate { private void runTestcontainerCassandra() { if (container == null) { - String cassandra_version = System.getenv("CASSANDRA_VERSION"); - if (StringUtils.hasText(cassandra_version)) { - container = new CassandraContainer<>("cassandra:" + cassandra_version); - } else { - container = new CassandraContainer<>(); - } + + container = getCassandraDockerImageName().map(CassandraContainer::new) + .orElseGet(CassandraContainer::new); container.start(); @@ -278,6 +280,13 @@ class CassandraDelegate { } } + private Optional getCassandraDockerImageName() { + + return Optional.ofNullable(System.getenv("CASSANDRA_VERSION")) + .filter(StringUtils::hasText) + .map(cassandraVersion -> String.format("cassandra:%s", cassandraVersion)); + } + private synchronized void initializeConnection() { this.cassandraPort = resolvePort(); @@ -420,7 +429,8 @@ class CassandraDelegate { /** * Create a {@link CqlSession} object. * - * @return + * @return a new {@link CqlSession}. + * @see com.datastax.oss.driver.api.core.CqlSession */ CqlSession createSession() { return sessionBuilder.build(); diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/CassandraExtension.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/CassandraExtension.java index 9aa84674c..8efdb57c4 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/CassandraExtension.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/CassandraExtension.java @@ -19,7 +19,6 @@ import java.lang.reflect.Field; import java.util.List; import java.util.Optional; -import org.apache.commons.lang3.reflect.FieldUtils; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.ExtensionContext; @@ -28,6 +27,8 @@ import org.junit.platform.commons.util.AnnotationUtils; import org.junit.platform.commons.util.ReflectionUtils; import org.junit.platform.commons.util.StringUtils; +import org.apache.commons.lang3.reflect.FieldUtils; + import org.springframework.data.cassandra.support.RandomKeyspaceName; import org.springframework.util.Assert; @@ -62,16 +63,6 @@ public class CassandraExtension implements BeforeAllCallback, AfterAllCallback, private final CassandraDelegate delegate = new CassandraDelegate("embedded-cassandra.yaml"); - /** - * Retrieve the system {@link CqlSession} that is associated with the current {@link Thread}. - * - * @return the system {@link CqlSession}. - * @throws IllegalStateException if no system session was associated with the current {@link Thread}. - */ - public static CqlSession currentSystemSession() { - return getResources().systemSession; - } - /** * Retrieve the keyspace {@link CqlSession} that is associated with the current {@link Thread}. * @@ -82,28 +73,36 @@ public class CassandraExtension implements BeforeAllCallback, AfterAllCallback, CqlSession cqlSession = getResources().cqlSession; - if (cqlSession == null) { - throw new IllegalStateException( - "No keyspace-bound session. Make sure to annotate your test class with @TestKeyspaceName"); - } + Assert.state(cqlSession != null, + "No keyspace-bound session. Make sure to annotate your test class with @TestKeyspaceName"); return cqlSession; } + /** + * Retrieve the system {@link CqlSession} that is associated with the current {@link Thread}. + * + * @return the system {@link CqlSession}. + * @throws IllegalStateException if no system session was associated with the current {@link Thread}. + */ + public static CqlSession currentSystemSession() { + return getResources().systemSession; + } + public static Resources getResources() { + Resources resources = TEST_RESOURCES.get(); - if (resources == null) { - throw new IllegalStateException( - "No test in progress. Did you annotate your test class with @ExtendWith(CassandraExtension.class)?"); - } + Assert.state(resources != null, + "No test in progress. Did you annotate your test class with @ExtendWith(CassandraExtension.class)?"); + return resources; } @Override public void beforeAll(ExtensionContext context) throws Exception { - Optional keyspaceName = getKeyspaceName(context.getTestClass()); + Optional keyspaceName = getKeyspaceName(context); ExtensionContext.Store store = context.getStore(CASSANDRA); delegate.before(); @@ -115,7 +114,6 @@ public class CassandraExtension implements BeforeAllCallback, AfterAllCallback, if (keyspaceName.isPresent()) { keyspaceName.ifPresent(name -> { - store.put("KEYSPACE", name); TestKeyspaceDelegate.before(session, name); store.put(CqlSession.class, session); @@ -129,6 +127,7 @@ public class CassandraExtension implements BeforeAllCallback, AfterAllCallback, TEST_RESOURCES.remove(); ExtensionContext.Store store = context.getStore(CASSANDRA); + String keyspaceName = store.getOrDefault("KEYSPACE", String.class, null); if (keyspaceName != null) { @@ -148,14 +147,16 @@ public class CassandraExtension implements BeforeAllCallback, AfterAllCallback, @Override public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception { - ExtensionContext.Store store = context.getStore(CASSANDRA); - List fields = ReflectionUtils.findFields(testInstance.getClass(), field -> AnnotationUtils.isAnnotated(field, TestKeyspace.class), ReflectionUtils.HierarchyTraversalMode.TOP_DOWN); if (!fields.isEmpty()) { + + ExtensionContext.Store store = context.getStore(CASSANDRA); + String keyspaceName = store.get("KEYSPACE", String.class); + CqlSession cqlSession = store.get(CqlSession.class, CqlSession.class); Assert.state(!cqlSession.isClosed(), "CQL Session closed"); @@ -172,14 +173,16 @@ public class CassandraExtension implements BeforeAllCallback, AfterAllCallback, } } - private Optional getKeyspaceName(Optional> instance) { + private Optional getKeyspaceName(ExtensionContext context) { + + return context.getTestClass().map(it -> { - return instance.map(it -> { Optional annotation = AnnotationUtils.findAnnotation(it, TestKeyspaceName.class); if (annotation.isPresent()) { - return annotation.map(TestKeyspaceName::value).filter(StringUtils::isNotBlank) - .orElseGet(RandomKeyspaceName::create); + return annotation.map(TestKeyspaceName::value) + .filter(StringUtils::isNotBlank) + .orElseGet(RandomKeyspaceName::create); } return null; diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/EmbeddedCassandraServerHelper.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/EmbeddedCassandraServerHelper.java index 255d833b1..9142ae379 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/EmbeddedCassandraServerHelper.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/util/EmbeddedCassandraServerHelper.java @@ -44,9 +44,10 @@ import org.springframework.util.FileSystemUtils; * * @author Mark Paluch */ +@SuppressWarnings("unused") class EmbeddedCassandraServerHelper { - private static Logger log = LoggerFactory.getLogger(EmbeddedCassandraServerHelper.class); + private static final Logger log = LoggerFactory.getLogger(EmbeddedCassandraServerHelper.class); public static final long DEFAULT_STARTUP_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(20); private static final String DEFAULT_TMP_DIR = "target/embeddedCassandra"; @@ -95,9 +96,10 @@ class EmbeddedCassandraServerHelper { /** * Start an embedded Cassandra instance. * - * @param yamlResource - * @param timeout - * @throws Exception + * @param yamlResource {@link String location} of a YAML file to configure Cassandra. + * @param timeout {@link Long} value specifying the timeout used to wait for the Cassandra server to startup. + * @throws Exception if the Cassandra server fails to start. + * @see #startEmbeddedCassandra(String, String, long) */ public static void startEmbeddedCassandra(String yamlResource, long timeout) throws Exception { startEmbeddedCassandra(yamlResource, DEFAULT_TMP_DIR, timeout); @@ -106,10 +108,10 @@ class EmbeddedCassandraServerHelper { /** * Start an embedded Cassandra instance. * - * @param yamlResource - * @param tmpDir - * @param timeout - * @throws Exception + * @param yamlResource {@link String resource location} of a YAML file to configure Cassandra. + * @param tmpDir {@link String filesystem location} used by the embedded Cassandra instance. + * @param timeout {@link Long} value specifying the timeout used to wait for the Cassandra server to startup. + * @throws Exception if the Cassandra server fails to start. */ private static void startEmbeddedCassandra(String yamlResource, String tmpDir, long timeout) throws Exception {