/* * Copyright 2016-2017 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 org.springframework.cassandra.test.integration; import static org.springframework.cassandra.test.integration.CassandraRule.InvocationMode.*; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import org.junit.rules.ExternalResource; import org.springframework.cassandra.core.SessionCallback; import org.springframework.cassandra.test.integration.support.CassandraConnectionProperties; import org.springframework.cassandra.test.integration.support.CqlDataSet; import org.springframework.cassandra.test.integration.support.IntegrationTestNettyOptions; import org.springframework.util.Assert; import org.springframework.util.SocketUtils; import com.datastax.driver.core.Cluster; import com.datastax.driver.core.QueryOptions; import com.datastax.driver.core.Session; import com.datastax.driver.core.SocketOptions; /** * Rule to provide a Cassandra context for integration tests. This rule can use/spin up either an embedded Cassandra * instance or use an external instance. Typical usage: * * <pre> * { * public class MyIntegrationTest { * @Rule public CassandraRule rule = new CassandraRule(CONFIG). // * before(new ClassPathCQLDataSet("CreateIndexCqlGeneratorIntegrationTests-BasicTest.cql", "keyspace")); * } * } * </pre> * * @author Mark Paluch * @since 1.5 */ public class CassandraRule extends ExternalResource { private static ResourceHolder resourceHolder; private final CassandraConnectionProperties properties = new CassandraConnectionProperties(); private final String configurationFileName; private final long startUpTimeout; private List<SessionCallback<Void>> before = new ArrayList<>(); private Map<SessionCallback<?>, InvocationMode> invocationModeMap = new HashMap<>(); private List<SessionCallback<Void>> after = new ArrayList<>(); private Session session; private Cluster cluster; private CassandraRule parent; private Integer cassandraPort; /** * Create a new {@link CassandraRule} and allows the use of a config file. * * @param yamlConfigurationResource name of the configuration resource, must not be {@literal null} and not empty */ public CassandraRule(String yamlConfigurationResource) { this(yamlConfigurationResource, EmbeddedCassandraServerHelper.DEFAULT_STARTUP_TIMEOUT_MS); } /** * Create a new {@link CassandraRule}, allows the use of a config file and to provide a startup timeout. * * @param yamlConfigurationResource name of the configuration resource, must not be {@literal null} and not empty * @param startUpTimeout the startup timeout */ public CassandraRule(String yamlConfigurationResource, long startUpTimeout) { Assert.hasText(yamlConfigurationResource, "Configuration file name must not be empty!"); this.configurationFileName = yamlConfigurationResource; this.startUpTimeout = startUpTimeout; } /** * Create a new {@link CassandraRule} using a parent {@link CassandraRule} to preserve cluster/connection facilities. * * @param parent the parent instance */ private CassandraRule(CassandraRule parent) { this.configurationFileName = null; this.startUpTimeout = -1; this.parent = parent; } /** * Add a {@link CqlDataSet} to execute before each test run. * * @param cqlDataSet must not be {@literal null} * @return the rule */ public CassandraRule before(CqlDataSet cqlDataSet) { return before(each(), cqlDataSet); } /** * Add a {@link CqlDataSet} to execute before the test run. * * @param invocationMode must not be {@literal null} * @param cqlDataSet must not be {@literal null} * @return the rule */ public CassandraRule before(InvocationMode invocationMode, final CqlDataSet cqlDataSet) { Assert.notNull(cqlDataSet, "CQLDataSet must not be null"); SessionCallback<Void> sessionCallback = session -> { load(session, cqlDataSet); return null; }; before(invocationMode, sessionCallback); return this; } /** * Add a {@link SessionCallback} to execute before each test run. * * @param sessionCallback must not be {@literal null} * @return the rule */ public CassandraRule before(final SessionCallback<?> sessionCallback) { Assert.notNull(sessionCallback, "SessionCallback must not be null"); return before(each(), sessionCallback); } /** * Add a {@link SessionCallback} to execute before the test run. * * @param invocationMode must not be {@literal null} * @param sessionCallback must not be {@literal null} * @return the rule */ @SuppressWarnings("unchecked") public CassandraRule before(InvocationMode invocationMode, final SessionCallback<?> sessionCallback) { Assert.notNull(sessionCallback, "SessionCallback must not be null"); before.add((SessionCallback<Void>) sessionCallback); invocationModeMap.put(sessionCallback, invocationMode); return this; } /** * Add a {@link CqlDataSet} to execute before the test run. * * @param cqlDataSet must not be {@literal null} * @return the rule */ public CassandraRule after(final CqlDataSet cqlDataSet) { Assert.notNull(cqlDataSet, "CQLDataSet must not be null"); after.add(session -> { load(CassandraRule.this.session, cqlDataSet); return null; }); return this; } /** * Execute a {@link CqlDataSet}. * * @param cqlDataSet the CQL data set, must not be {@literal null}. */ public void execute(CqlDataSet cqlDataSet) { Assert.notNull(cqlDataSet, "CQLDataSet must not be null"); load(session, cqlDataSet); } /** * Execute the {@code before} sequence. * * @throws Exception */ @Override public void before() throws Exception { startCassandraIfNeeded(); setupConnection(); executeBeforeHooks(); } /** * Execute the {@code after} sequence. */ @Override protected void after() { super.after(); executeAfterHooks(); cleanupConnection(); } /** * Returns the {@link Cluster}. * * @return the Cluster */ public Cluster getCluster() { return cluster; } /** * Returns the {@link Session}. The session state can be initialized and pointing to a keyspace other than * {@code system}. * * @return the Session */ public Session getSession() { return session; } /** * Returns the Cassandra port. * * @return the Cassandra port */ public int getPort() { Assert.state(cassandraPort != null, "Cassandra port is not initialized"); return cassandraPort; } /** * Creates a {@link CassandraRule} to be used in a own scope. The derived {@link CassandraRule} shares the connection * of this instance and starts with a fresh before/after configuration. * * @return a derived {@link CassandraRule} sharing the connection of this instance */ public CassandraRule testInstance() { return new CassandraRule(this); } private void startCassandraIfNeeded() throws Exception { if (parent == null && properties.getCassandraType() == CassandraConnectionProperties.CassandraType.EMBEDDED) { /* start an embedded Cassandra instance*/ if (!System.getProperties().containsKey("com.sun.management.jmxremote.port")) { System.setProperty("com.sun.management.jmxremote.port", "" + SocketUtils.findAvailableTcpPort(1024)); } if (configurationFileName != null) { EmbeddedCassandraServerHelper.startEmbeddedCassandra(configurationFileName, startUpTimeout); } } } private void executeBeforeHooks() { for (SessionCallback<Void> sessionCallback : before) { InvocationMode invocationMode = invocationModeMap.get(sessionCallback); if (invocationMode == never()) { continue; } if (invocationMode == firstTest()) { invocationModeMap.put(sessionCallback, never()); } sessionCallback.doInSession(session); } } private void executeAfterHooks() { for (SessionCallback<Void> sessionCallback : after) { sessionCallback.doInSession(session); } } private void setupConnection() { if (parent == null) { String hostIp; int port; if (properties.getCassandraType() == CassandraConnectionProperties.CassandraType.EMBEDDED) { hostIp = EmbeddedCassandraServerHelper.getHost(); port = EmbeddedCassandraServerHelper.getNativeTransportPort(); } else { hostIp = properties.getCassandraHost(); port = properties.getCassandraPort(); } cassandraPort = port; QueryOptions queryOptions = new QueryOptions(); queryOptions.setRefreshSchemaIntervalMillis(0); SocketOptions socketOptions = new SocketOptions(); socketOptions.setConnectTimeoutMillis((int) TimeUnit.SECONDS.toMillis(15)); socketOptions.setReadTimeoutMillis((int) TimeUnit.SECONDS.toMillis(15)); if (resourceHolder == null) { cluster = new Cluster.Builder().addContactPoints(hostIp) // .withPort(port) // .withQueryOptions(queryOptions) // .withMaxSchemaAgreementWaitSeconds(3) // .withSocketOptions(socketOptions) // .withNettyOptions(IntegrationTestNettyOptions.INSTANCE) // .build(); if (properties.getBoolean("build.cassandra.reuse-cluster")) { resourceHolder = new ResourceHolder(cluster, cluster.connect()); } } else { cluster = resourceHolder.cluster; } } else { cluster = parent.cluster; cassandraPort = parent.cassandraPort; } if (parent != null) { session = parent.getSession(); } else if (resourceHolder == null) { session = cluster.connect(); } else { session = resourceHolder.session; } } private void cleanupConnection() { if (resourceHolder == null) { if (parent == null) { session.close(); cluster.closeAsync(); cluster = null; } else { session.closeAsync(); } } session = null; } private void load(Session session, final CqlDataSet cqlDataSet) { if (cqlDataSet.getKeyspaceName() != null && !cqlDataSet.getKeyspaceName().equals(session.getLoggedKeyspace())) { session.execute(String.format("USE %s;", cqlDataSet.getKeyspaceName())); } cqlDataSet.getCqlStatements().forEach(session::execute); } /** * Invocation mode for before calls. */ public static class InvocationMode { private static final InvocationMode once = new InvocationMode(); private static final InvocationMode each = new InvocationMode(); private static final InvocationMode never = new InvocationMode(); /** * Invocation mode to invoke an action once at before the first test. * * @return the {@code on first test} invocation mode */ public static InvocationMode firstTest() { return once; } /** * Invocation mode to invoke an action on each run. * * @return the {@code on each test} invocation mode */ public static InvocationMode each() { return each; } /** * Invocation mode to never invoke an action. * * @return the {@code never} invocation mode */ static InvocationMode never() { return never; } private InvocationMode() { } } private static class ResourceHolder { private Cluster cluster; private Session session; public ResourceHolder(final Cluster cluster, final Session session) { this.cluster = cluster; this.session = session; Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { session.close(); cluster.close(); } }); } } }