+ String[] parts = trimmed.split(",", 2);
+ if (parts.length == 2) {
+ return parts[1].trim();
+ }
+ }
+ }
+ throw new IllegalStateException("Could not extract database name from CSV output: " + output);
+ }
+
+ /**
+ * Checks if the error message indicates a transient error that should be retried.
+ *
+ * @param errorMessage the error message from the failed command
+ * @return true if the error is retryable
+ */
+ private boolean isRetryableAstraError(String errorMessage) {
+ if (errorMessage == null) {
+ return false;
+ }
+ // Check for invalid state errors (e.g., MAINTENANCE mode)
+ if (errorMessage.contains("invalid state")) {
+ return true;
+ }
+ // Check for internal HTTP errors
+ if (errorMessage.contains("INTERNAL_ERROR") && errorMessage.contains("HTTP Request")) {
+ return true;
+ }
+ // Check for HttpEntity errors
+ if (errorMessage.contains("HttpEntity")) {
+ return true;
+ }
+ return false;
+ }
+
+ private String runAstraCommand(String... args) throws IOException, InterruptedException {
+ int maxRetries = 3;
+ int retryDelaySeconds = 10;
+ IOException lastException = null;
+
+ for (int attempt = 0; attempt <= maxRetries; attempt++) {
+ try {
+ return executeAstraCommand(args);
+ } catch (IOException e) {
+ lastException = e;
+ String errorMessage = e.getMessage();
+
+ // Check if this is a retryable error
+ if (isRetryableAstraError(errorMessage) && attempt < maxRetries) {
+ LOG.error(
+ "Astra CLI command failed with transient error (attempt {}/{}): {}",
+ attempt + 1,
+ maxRetries + 1,
+ errorMessage);
+ LOG.info("Waiting {} seconds before retry...", retryDelaySeconds);
+
+ try {
+ Thread.sleep(retryDelaySeconds * 1000L);
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ throw new IOException("Interrupted while waiting to retry Astra CLI command", ie);
+ }
+ // Continue to next retry iteration
+ } else {
+ // Non-retryable error or max retries reached, throw immediately
+ throw e;
+ }
+ }
+ }
+
+ // Should not reach here, but just in case
+ throw lastException;
+ }
+
+ private String executeAstraCommand(String... args) throws IOException, InterruptedException {
+ // Build command line
+ CommandLine cli = new CommandLine("astra");
+ for (String arg : args) {
+ cli.addArgument(arg);
+ }
+
+ LOG.info("Running Astra CLI command: {}", cli.toString());
+
+ // StringBuilder to collect output
+ StringBuilder output = new StringBuilder();
+
+ // Create watchdog with 10-minute timeout (same as CcmBridge)
+ ExecuteWatchdog watchDog = new ExecuteWatchdog(TimeUnit.MINUTES.toMillis(10));
+
+ try (LogOutputStream outStream =
+ new LogOutputStream() {
+ @Override
+ protected void processLine(String line, int logLevel) {
+ output.append(line).append("\n");
+ LOG.debug("astraout> {}", line);
+ }
+ };
+ LogOutputStream errStream =
+ new LogOutputStream() {
+ @Override
+ protected void processLine(String line, int logLevel) {
+ output.append(line).append("\n");
+ LOG.error("astraerr> {}", line);
+ }
+ }) {
+
+ Executor executor = new DefaultExecutor();
+ ExecuteStreamHandler streamHandler = new PumpStreamHandler(outStream, errStream);
+ executor.setStreamHandler(streamHandler);
+ executor.setWatchdog(watchDog);
+
+ int retValue = executor.execute(cli);
+
+ if (retValue != 0) {
+ throw new IOException(
+ "Astra CLI command failed with exit code "
+ + retValue
+ + ": "
+ + cli.toString()
+ + "\nOutput: "
+ + output);
+ }
+
+ } catch (IOException e) {
+ if (watchDog.killedProcess()) {
+ throw new IOException("Astra CLI command timed out after 10 minutes: " + cli.toString(), e);
+ }
+ throw e;
+ }
+
+ return output.toString();
+ }
+
+ @Override
+ public synchronized void start() {
+ if (started.compareAndSet(false, true)) {
+ create();
+ }
+ }
+
+ @Override
+ public synchronized void stop() {
+ if (databaseId == null) {
+ LOG.info("No Astra database to terminate");
+ return;
+ }
+
+ if (usingExistingDatabase) {
+ LOG.info("Using existing Astra database (ID: {}), skipping deletion", databaseId);
+ return;
+ }
+
+ try {
+ LOG.info("Terminating Astra database: {} (ID: {})", databaseName, databaseId);
+
+ // Delete the database asynchronously (don't wait for completion)
+ String deleteOutput = runAstraCommand("db", "delete", databaseName, "--yes");
+ LOG.info("Database deletion initiated: {}", deleteOutput);
+ LOG.info("Astra database {} (ID: {}) is being terminated", databaseName, databaseId);
+ } catch (Exception e) {
+ LOG.warn("Failed to terminate Astra database {}: {}", databaseName, e.getMessage());
+ LOG.info("To terminate manually, run: astra db delete {}", databaseName);
+ }
+ }
+
+ @Override
+ public void close() {
+ stop();
+ }
+
+ public String getKeyspace() {
+ return keyspace;
+ }
+
+ public File getSecureConnectBundle() {
+ return secureConnectBundle;
+ }
+
+ public String getToken() {
+ return ASTRA_TOKEN;
+ }
+
+ @Override
+ public BackendType getDistribution() {
+ return DISTRIBUTION;
+ }
+
+ /**
+ * Drops all user-created tables in the specified keyspace. This is used to clean up after test
+ * suites when running against Astra, where creating/dropping keyspaces is expensive.
+ *
+ * System tables (those starting with "system") are not dropped.
+ *
+ * @param keyspaceName the name of the keyspace to clean
+ * @param session the CQL session to use for dropping tables
+ */
+ public void dropAllTablesInKeyspace(String keyspaceName, CqlSession session) {
+ if (databaseName == null) {
+ throw new IllegalStateException(
+ "Cannot drop tables: Astra database has not been created yet");
+ }
+
+ try {
+ LOG.info(
+ "Dropping all tables in keyspace '{}' of Astra database '{}'",
+ keyspaceName,
+ databaseName);
+
+ // Get keyspace metadata from the driver
+ Optional keyspaceMetadata = session.getMetadata().getKeyspace(keyspaceName);
+
+ if (!keyspaceMetadata.isPresent()) {
+ LOG.error("Keyspace '{}' not found in metadata", keyspaceName);
+ return;
+ }
+
+ // Get all tables and UDTs from the keyspace metadata
+ Map tables = keyspaceMetadata.get().getTables();
+ Map udts = keyspaceMetadata.get().getUserDefinedTypes();
+
+ // IMPORTANT: Drop tables FIRST, then UDTs
+ // UDTs cannot be dropped while tables are still using them
+
+ // Step 1: Drop all tables
+ AtomicInteger droppedTableCount = new AtomicInteger();
+ for (TableMetadata tableMetadata : tables.values()) {
+ CqlIdentifier tableId = tableMetadata.getName();
+ String tableName = tableId.asInternal();
+ if (!tableName.startsWith("system")) {
+ try {
+ // IMPORTANT: Use the CqlIdentifier to properly quote the table name
+ // Tables created with quotes (e.g., "UPPER_CASE") need quotes to drop
+ String dropStatement =
+ String.format("DROP TABLE IF EXISTS %s.%s", keyspaceName, tableId.asCql(true));
+ session.execute(
+ SimpleStatement.newInstance(dropStatement)
+ .setTimeout(Duration.of(20, ChronoUnit.SECONDS)));
+ droppedTableCount.getAndIncrement();
+ LOG.info("Dropped table '{}.{}' using: {}", keyspaceName, tableName, dropStatement);
+ } catch (DriverException e) {
+ LOG.error("Failed to drop table '{}.{}': {}", keyspaceName, tableName, e.getMessage());
+ }
+ }
+ }
+
+ LOG.info("Dropped {} tables in keyspace '{}'", droppedTableCount.get(), keyspaceName);
+
+ // Step 2: Drop all UDTs (after tables are dropped)
+ AtomicInteger droppedUdtCount = new AtomicInteger();
+ for (UserDefinedType udt : udts.values()) {
+ CqlIdentifier udtId = udt.getName();
+ String udtName = udtId.asInternal();
+ try {
+ // IMPORTANT: Use the CqlIdentifier to properly quote the UDT name
+ String dropStatement =
+ String.format("DROP TYPE IF EXISTS %s.%s", keyspaceName, udtId.asCql(true));
+ session.execute(
+ SimpleStatement.newInstance(dropStatement)
+ .setTimeout(Duration.of(20, ChronoUnit.SECONDS)));
+ droppedUdtCount.getAndIncrement();
+ LOG.info("Dropped type '{}.{}' using: {}", keyspaceName, udtName, dropStatement);
+ } catch (DriverException e) {
+ LOG.error("Failed to drop type '{}.{}': {}", keyspaceName, udtName, e.getMessage());
+ }
+ }
+ } catch (Exception e) {
+ LOG.error("Failed to drop tables in keyspace '{}': {}", keyspaceName, e.getMessage(), e);
+ // Don't throw - this is cleanup, we don't want to fail tests because of cleanup issues
+ }
+ }
+}
diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/astra/AstraRule.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/astra/AstraRule.java
new file mode 100644
index 00000000000..12640cf6fdf
--- /dev/null
+++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/astra/AstraRule.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 com.datastax.oss.driver.api.testinfra.astra;
+
+import com.datastax.oss.driver.categories.ParallelizableTests;
+import org.junit.AssumptionViolatedException;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * A rule that creates a globally shared Astra database that is only terminated after the JVM exits.
+ *
+ * Note that this rule should be considered mutually exclusive with {@link CustomAstraRule}.
+ * Creating instances of these rules can create resource issues.
+ */
+public class AstraRule extends BaseAstraRule {
+
+ private static volatile AstraRule INSTANCE;
+
+ private volatile boolean started = false;
+
+ private AstraRule() {
+ super(AstraBridge.builder().build());
+ }
+
+ @Override
+ protected synchronized void before() {
+ if (!started) {
+ // synchronize before so blocks on other before() call waiting to finish.
+ super.before();
+ started = true;
+ }
+ }
+
+ @Override
+ protected void after() {
+ // override after so we don't remove when done.
+ }
+
+ @Override
+ public Statement apply(Statement base, Description description) {
+
+ Category categoryAnnotation = description.getTestClass().getAnnotation(Category.class);
+ if (categoryAnnotation == null
+ || categoryAnnotation.value().length != 1
+ || categoryAnnotation.value()[0] != ParallelizableTests.class) {
+ return new Statement() {
+ @Override
+ public void evaluate() {
+ throw new AssumptionViolatedException(
+ String.format(
+ "Tests using %s must be annotated with `@Category(%s.class)`. Description: %s",
+ AstraRule.class.getSimpleName(),
+ ParallelizableTests.class.getSimpleName(),
+ description));
+ }
+ };
+ }
+
+ return super.apply(base, description);
+ }
+
+ public static AstraRule getInstance() {
+ // Lazy initialization to avoid creating AstraBridge when not using Astra
+ if (INSTANCE == null) {
+ synchronized (AstraRule.class) {
+ if (INSTANCE == null) {
+ INSTANCE = new AstraRule();
+ }
+ }
+ }
+ return INSTANCE;
+ }
+}
diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/astra/BaseAstraRule.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/astra/BaseAstraRule.java
new file mode 100644
index 00000000000..b49f7c3e6ca
--- /dev/null
+++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/astra/BaseAstraRule.java
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 com.datastax.oss.driver.api.testinfra.astra;
+
+import com.datastax.oss.driver.api.core.CqlSession;
+import com.datastax.oss.driver.api.core.DefaultProtocolVersion;
+import com.datastax.oss.driver.api.core.ProtocolVersion;
+import com.datastax.oss.driver.api.core.Version;
+import com.datastax.oss.driver.api.core.metadata.EndPoint;
+import com.datastax.oss.driver.api.testinfra.ccm.CcmRule;
+import com.datastax.oss.driver.api.testinfra.requirement.BackendRequirementRule;
+import com.datastax.oss.driver.api.testinfra.requirement.BackendType;
+import java.io.File;
+import java.util.Collections;
+import java.util.Set;
+import org.junit.AssumptionViolatedException;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+import org.slf4j.LoggerFactory;
+
+public abstract class BaseAstraRule extends CcmRule {
+
+ protected final AstraBridge astraBridge;
+
+ // Reusable session for table cleanup operations
+ private volatile CqlSession cleanupSession;
+
+ BaseAstraRule(AstraBridge astraBridge) {
+ super();
+ this.astraBridge = astraBridge;
+ Runtime.getRuntime()
+ .addShutdownHook(
+ new Thread(
+ () -> {
+ try {
+ closeCleanupSession();
+ astraBridge.close();
+ } catch (Exception e) {
+ // silently remove as may have already been removed.
+ }
+ }));
+ }
+
+ @Override
+ protected synchronized void before() {
+ astraBridge.create();
+ astraBridge.start();
+ }
+
+ @Override
+ public Statement apply(Statement base, Description description) {
+ if (BackendRequirementRule.meetsDescriptionRequirements(description)) {
+ // @ClassRule: Wrap the base statement to drop all tables after test suite execution
+ Statement wrappedStatement =
+ new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ try {
+ base.evaluate();
+ } finally {
+ // Drop all tables in the keyspace after the test suite completes
+ dropAllTablesInKeyspace();
+ }
+ }
+ };
+ return super.apply(wrappedStatement, description);
+ } else {
+ // requirements not met, throw reasoning assumption to skip test
+ return new Statement() {
+ @Override
+ public void evaluate() {
+ throw new AssumptionViolatedException(
+ BackendRequirementRule.buildReasonString(description));
+ }
+ };
+ }
+ }
+
+ /**
+ * Gets or creates a reusable CQL session for cleanup operations. This session is created lazily
+ * and reused across all table cleanup operations to avoid the overhead of creating/closing
+ * sessions after each test suite.
+ *
+ * @return a CQL session connected to the Astra database
+ */
+ private synchronized CqlSession getOrCreateCleanupSession() {
+ if (cleanupSession == null || cleanupSession.isClosed()) {
+ String keyspaceName = astraBridge.getKeyspace();
+ cleanupSession =
+ CqlSession.builder()
+ .withCloudSecureConnectBundle(astraBridge.getSecureConnectBundle().toPath())
+ .withAuthCredentials("token", astraBridge.getToken())
+ .withKeyspace(keyspaceName)
+ .build();
+ LoggerFactory.getLogger(BaseAstraRule.class)
+ .error("Created reusable cleanup session for keyspace '{}'", keyspaceName);
+ }
+ return cleanupSession;
+ }
+
+ /**
+ * Closes the cleanup session if it exists. This is called when the rule is done (either in
+ * after() or in the shutdown hook).
+ */
+ private synchronized void closeCleanupSession() {
+ if (cleanupSession != null && !cleanupSession.isClosed()) {
+ try {
+ LoggerFactory.getLogger(BaseAstraRule.class)
+ .error("Closing cleanup session for keyspace '{}'", astraBridge.getKeyspace());
+ cleanupSession.close();
+ } catch (Exception e) {
+ LoggerFactory.getLogger(BaseAstraRule.class)
+ .error("Failed to close cleanup session: {}", e.getMessage(), e);
+ }
+ cleanupSession = null;
+ }
+ }
+
+ /**
+ * Drops all user-created tables in the keyspace after a test suite completes. This is called
+ * automatically after each test class when running against Astra to avoid the expensive operation
+ * of creating/dropping keyspaces.
+ *
+ *
This method uses a reusable session that is created once and reused across all cleanup
+ * operations.
+ */
+ protected void dropAllTablesInKeyspace() {
+ String keyspaceName = astraBridge.getKeyspace();
+ if (keyspaceName == null || keyspaceName.isEmpty()) {
+ return; // No keyspace to clean
+ }
+
+ try {
+ CqlSession session = getOrCreateCleanupSession();
+ astraBridge.dropAllTablesInKeyspace(keyspaceName, session);
+ } catch (Exception e) {
+ // Log but don't fail - this is cleanup
+ LoggerFactory.getLogger(BaseAstraRule.class)
+ .error("Failed to drop tables in keyspace '{}': {}", keyspaceName, e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public BackendType getDistribution() {
+ return AstraBridge.DISTRIBUTION;
+ }
+
+ @Override
+ public boolean isDistributionOf(BackendType type) {
+ return AstraBridge.isDistributionOf(type);
+ }
+
+ @Override
+ public Version getDistributionVersion() {
+ return AstraBridge.getDistributionVersion();
+ }
+
+ @Override
+ public Version getCassandraVersion() {
+ return AstraBridge.getCassandraVersion();
+ }
+
+ @Override
+ public ProtocolVersion getHighestProtocolVersion() {
+ // Astra supports protocol version V4
+ return DefaultProtocolVersion.V4;
+ }
+
+ @Override
+ public Set getContactPoints() {
+ // Astra uses Secure Connect Bundle instead of contact points
+ return Collections.emptySet();
+ }
+
+ /**
+ * Returns the Secure Connect Bundle file for connecting to Astra.
+ *
+ * @return the Secure Connect Bundle file
+ */
+ public File getSecureConnectBundle() {
+ return astraBridge.getSecureConnectBundle();
+ }
+
+ public AstraBridge getAstraBridge() {
+ return astraBridge;
+ }
+}
diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/astra/CustomAstraRule.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/astra/CustomAstraRule.java
new file mode 100644
index 00000000000..c474d2add61
--- /dev/null
+++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/astra/CustomAstraRule.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 com.datastax.oss.driver.api.testinfra.astra;
+
+import java.util.concurrent.atomic.AtomicReference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A rule that creates an Astra database that can be used in a test. This should be used if you plan
+ * on creating databases with unique configurations, such as different cloud providers, regions, or
+ * keyspaces. If you do not plan on doing this at all in your tests, consider using {@link
+ * AstraRule} which creates a global Astra database that may be shared among tests.
+ *
+ * Note that this rule should be considered mutually exclusive with {@link AstraRule}. Creating
+ * instances of these rules can create resource issues.
+ */
+public class CustomAstraRule extends BaseAstraRule {
+
+ private static final Logger LOG = LoggerFactory.getLogger(CustomAstraRule.class);
+ private static final AtomicReference CURRENT = new AtomicReference<>();
+
+ CustomAstraRule(AstraBridge astraBridge) {
+ super(astraBridge);
+ }
+
+ @Override
+ protected synchronized void before() {
+ if (CURRENT.get() == null && CURRENT.compareAndSet(null, this)) {
+ try {
+ super.before();
+ } catch (Exception e) {
+ // ExternalResource will not call after() when before() throws an exception
+ // Let's try and clean up and release the lock we have in CURRENT
+ LOG.warn(
+ "Error in CustomAstraRule before() method, attempting to clean up leftover state", e);
+ try {
+ after();
+ } catch (Exception e1) {
+ LOG.warn("Error cleaning up CustomAstraRule before() failure", e1);
+ e.addSuppressed(e1);
+ }
+ throw e;
+ }
+ } else if (CURRENT.get() != this) {
+ throw new IllegalStateException(
+ "Attempting to use an Astra rule while another is in use. This is disallowed");
+ }
+ }
+
+ @Override
+ protected void after() {
+ try {
+ super.after();
+ } finally {
+ CURRENT.compareAndSet(this, null);
+ }
+ }
+
+ @Override
+ public AstraBridge getAstraBridge() {
+ return astraBridge;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+
+ private final AstraBridge.Builder bridgeBuilder = AstraBridge.builder();
+
+ public Builder withDatabaseName(String databaseName) {
+ bridgeBuilder.withDatabaseName(databaseName);
+ return this;
+ }
+
+ public Builder withKeyspace(String keyspace) {
+ bridgeBuilder.withKeyspace(keyspace);
+ return this;
+ }
+
+ public Builder withCloudProvider(String cloudProvider) {
+ bridgeBuilder.withCloudProvider(cloudProvider);
+ return this;
+ }
+
+ public Builder withRegion(String region) {
+ bridgeBuilder.withRegion(region);
+ return this;
+ }
+
+ public CustomAstraRule build() {
+ return new CustomAstraRule(bridgeBuilder.build());
+ }
+ }
+}
diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/BaseCcmRule.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/BaseCcmRule.java
index 882cd55b948..63dce14403c 100644
--- a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/BaseCcmRule.java
+++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/BaseCcmRule.java
@@ -31,7 +31,7 @@ public abstract class BaseCcmRule extends CassandraResourceRule {
protected final CcmBridge ccmBridge;
- BaseCcmRule(CcmBridge ccmBridge) {
+ protected BaseCcmRule(CcmBridge ccmBridge) {
this.ccmBridge = ccmBridge;
Runtime.getRuntime()
.addShutdownHook(
@@ -72,22 +72,26 @@ public void evaluate() {
}
}
+ @Override
public BackendType getDistribution() {
- return CcmBridge.DISTRIBUTION;
+ return ccmBridge.getDistribution();
}
+ @Override
public boolean isDistributionOf(BackendType type) {
- return CcmBridge.isDistributionOf(type);
+ return ccmBridge.getDistribution() == type;
}
public boolean isDistributionOf(BackendType type, CcmBridge.VersionComparator comparator) {
return CcmBridge.isDistributionOf(type, comparator);
}
+ @Override
public Version getDistributionVersion() {
return CcmBridge.getDistributionVersion();
}
+ @Override
public Version getCassandraVersion() {
return CcmBridge.getCassandraVersion();
}
diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CcmBridge.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CcmBridge.java
index f0ce6bc5b0e..5ab5f9af7b1 100644
--- a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CcmBridge.java
+++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CcmBridge.java
@@ -132,7 +132,7 @@ public class CcmBridge implements AutoCloseable {
private final List dseWorkloads;
private final String jvmArgs;
- private CcmBridge(
+ protected CcmBridge(
Path configDirectory,
int[] nodes,
String ipPrefix,
@@ -429,6 +429,10 @@ public void close() {
}
}
+ public BackendType getDistribution() {
+ return DISTRIBUTION;
+ }
+
/**
* Extracts a keystore from the classpath into a temporary file.
*
@@ -538,7 +542,7 @@ public static class Builder {
private final Path configDirectory;
- private Builder() {
+ protected Builder() {
try {
this.configDirectory = Files.createTempDirectory("ccm");
// mark the ccm temp directories for deletion when the JVM exits
diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CcmRule.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CcmRule.java
index e6483c37877..0fadea1679f 100644
--- a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CcmRule.java
+++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CcmRule.java
@@ -17,6 +17,8 @@
*/
package com.datastax.oss.driver.api.testinfra.ccm;
+import com.datastax.oss.driver.api.testinfra.astra.AstraRule;
+import com.datastax.oss.driver.api.testinfra.requirement.BackendType;
import com.datastax.oss.driver.categories.ParallelizableTests;
import java.lang.reflect.Method;
import org.junit.AssumptionViolatedException;
@@ -32,17 +34,31 @@
*
* Note that this rule should be considered mutually exclusive with {@link CustomCcmRule}.
* Creating instances of these rules can create resource issues.
+ *
+ *
When {@code ccm.distribution} system property is set to {@code ASTRA}, this rule will delegate
+ * to {@link AstraRule} instead of creating a CCM cluster.
*/
public class CcmRule extends BaseCcmRule {
- private static final CcmRule INSTANCE = new CcmRule();
+ private static volatile CcmRule CCM_INSTANCE;
+ private static final BackendType DISTRIBUTION =
+ BackendType.valueOf(
+ System.getProperty("ccm.distribution", BackendType.CASSANDRA.name()).toUpperCase());
private volatile boolean started = false;
- private CcmRule() {
+ protected CcmRule() {
super(configureCcmBridge(CcmBridge.builder()).build());
}
+ /**
+ * Protected constructor for subclasses (like AstraRule) that want to provide their own bridge
+ * implementation.
+ */
+ protected CcmRule(CcmBridge bridge) {
+ super(bridge);
+ }
+
public static CcmBridge.Builder configureCcmBridge(CcmBridge.Builder builder) {
Logger logger = LoggerFactory.getLogger(CcmRule.class);
String customizerClass =
@@ -99,7 +115,30 @@ public void evaluate() {
return super.apply(base, description);
}
+ /**
+ * Returns a singleton instance of a CCM rule.
+ *
+ *
The actual implementation returned depends on the {@code ccm.distribution} system property:
+ *
+ *
+ * - If set to {@code ASTRA}, returns {@link AstraRule#getInstance()} (which extends CcmRule)
+ *
- Otherwise, returns a {@link CcmRule} instance
+ *
+ *
+ * @return a singleton CCM rule
+ */
public static CcmRule getInstance() {
- return INSTANCE;
+ if (DISTRIBUTION == BackendType.ASTRA) {
+ return AstraRule.getInstance();
+ }
+ // Lazy initialization to avoid creating CcmBridge when using Astra
+ if (CCM_INSTANCE == null) {
+ synchronized (CcmRule.class) {
+ if (CCM_INSTANCE == null) {
+ CCM_INSTANCE = new CcmRule();
+ }
+ }
+ }
+ return CCM_INSTANCE;
}
}
diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CustomCcmRule.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CustomCcmRule.java
index 5ea1bf7ed3c..904ca997b56 100644
--- a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CustomCcmRule.java
+++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CustomCcmRule.java
@@ -17,7 +17,9 @@
*/
package com.datastax.oss.driver.api.testinfra.ccm;
+import com.datastax.oss.driver.api.testinfra.requirement.BackendType;
import java.util.concurrent.atomic.AtomicReference;
+import org.junit.Assume;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -41,6 +43,7 @@ public class CustomCcmRule extends BaseCcmRule {
@Override
protected void before() {
+ Assume.assumeTrue("Skipping for Astra", !isDistributionOf(BackendType.ASTRA));
if (CURRENT.get() == null && CURRENT.compareAndSet(null, this)) {
try {
super.before();
diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/DistributionCassandraVersions.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/DistributionCassandraVersions.java
index 9f7634d1b37..0c07bc9de29 100644
--- a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/DistributionCassandraVersions.java
+++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/DistributionCassandraVersions.java
@@ -25,6 +25,7 @@
/** Defines mapping of various distributions to shipped Apache Cassandra version. */
public abstract class DistributionCassandraVersions {
+
private static final Map> mappings =
new HashMap<>();
@@ -45,6 +46,13 @@ public abstract class DistributionCassandraVersions {
ImmutableSortedMap.of(Version.V1_0_0, CcmBridge.V4_0_11);
mappings.put(BackendType.HCD, hcd);
}
+ {
+ // Astra
+ // TODO: to be confirmed
+ ImmutableSortedMap astra =
+ ImmutableSortedMap.of(Version.V1_0_0, CcmBridge.V4_0_11);
+ mappings.put(BackendType.ASTRA, astra);
+ }
}
public static Version getCassandraVersion(BackendType type, Version version) {
diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/requirement/BackendRequirement.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/requirement/BackendRequirement.java
index 9b1400b6313..31478de4ea5 100644
--- a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/requirement/BackendRequirement.java
+++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/requirement/BackendRequirement.java
@@ -24,6 +24,15 @@
/**
* Annotation for a Class or Method that defines a database backend Version requirement. If the
* type/version in use does not meet the requirement, the test is skipped.
+ *
+ * When {@code include = true} (default), the test runs only if the backend type and version
+ * match the specified criteria.
+ *
+ *
When {@code include = false}, the test is skipped if the backend type matches (version
+ * constraints are ignored for exclusions).
+ *
+ *
Example: {@code @BackendRequirement(type = BackendType.ASTRA, include = false)} will skip the
+ * test when running with Astra.
*/
@Repeatable(BackendRequirements.class)
@Retention(RetentionPolicy.RUNTIME)
@@ -35,4 +44,10 @@
String maxExclusive() default "";
String description() default "";
+
+ /**
+ * Whether to include or exclude this backend type. When {@code true} (default), the test runs
+ * only if the backend matches. When {@code false}, the test is skipped if the backend matches.
+ */
+ boolean include() default true;
}
diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/requirement/BackendType.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/requirement/BackendType.java
index e0058ca324a..a1ff09ff3bd 100644
--- a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/requirement/BackendType.java
+++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/requirement/BackendType.java
@@ -20,7 +20,8 @@
public enum BackendType {
CASSANDRA("Apache Cassandra"),
DSE("DSE"),
- HCD("HCD");
+ HCD("HCD"),
+ ASTRA("Astra DB");
final String friendlyName;
@@ -33,7 +34,7 @@ public String getFriendlyName() {
}
public String[] getCcmOptions() {
- if (this == CASSANDRA) {
+ if (this == CASSANDRA || this == ASTRA) {
return new String[0];
}
return new String[] {"--" + name().toLowerCase()};
diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/requirement/VersionRequirement.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/requirement/VersionRequirement.java
index 6b184490a41..cde9cba75d2 100644
--- a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/requirement/VersionRequirement.java
+++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/requirement/VersionRequirement.java
@@ -36,15 +36,26 @@ public class VersionRequirement {
final Optional minInclusive;
final Optional maxExclusive;
final String description;
+ final boolean include;
public VersionRequirement(
BackendType backendType, String minInclusive, String maxExclusive, String description) {
+ this(backendType, minInclusive, maxExclusive, description, true);
+ }
+
+ public VersionRequirement(
+ BackendType backendType,
+ String minInclusive,
+ String maxExclusive,
+ String description,
+ boolean include) {
this.backendType = backendType;
this.minInclusive =
minInclusive.isEmpty() ? Optional.empty() : Optional.of(Version.parse(minInclusive));
this.maxExclusive =
maxExclusive.isEmpty() ? Optional.empty() : Optional.of(Version.parse(maxExclusive));
this.description = description;
+ this.include = include;
}
public BackendType getBackendType() {
@@ -60,6 +71,16 @@ public Optional getMaxExclusive() {
}
public String readableString() {
+ // For exclusions, just show "NOT "
+ if (!include) {
+ if (!description.isEmpty()) {
+ return String.format("NOT %s [%s]", backendType.getFriendlyName(), description);
+ } else {
+ return String.format("NOT %s", backendType.getFriendlyName());
+ }
+ }
+
+ // For inclusions, show version range
final String versionRange;
if (minInclusive.isPresent() && maxExclusive.isPresent()) {
versionRange =
@@ -84,7 +105,8 @@ public static VersionRequirement fromBackendRequirement(BackendRequirement requi
requirement.type(),
requirement.minInclusive(),
requirement.maxExclusive(),
- requirement.description());
+ requirement.description(),
+ requirement.include());
}
public static VersionRequirement fromCassandraRequirement(CassandraRequirement requirement) {
@@ -98,7 +120,8 @@ public static VersionRequirement fromDseRequirement(DseRequirement requirement)
}
public static Collection fromAnnotations(Description description) {
- // collect all requirement annotation types
+ // collect all requirement annotation types from both the method and the class
+ // (description.getAnnotation() only checks the method when using @Rule)
CassandraRequirement cassandraRequirement =
description.getAnnotation(CassandraRequirement.class);
DseRequirement dseRequirement = description.getAnnotation(DseRequirement.class);
@@ -107,6 +130,23 @@ public static Collection fromAnnotations(Description descrip
// matches methods/classes with two or more @BackendRequirement annotations
BackendRequirements backendRequirements = description.getAnnotation(BackendRequirements.class);
+ // Also check the test class for annotations (needed when using @Rule instead of @ClassRule)
+ Class> testClass = description.getTestClass();
+ if (testClass != null) {
+ if (cassandraRequirement == null) {
+ cassandraRequirement = testClass.getAnnotation(CassandraRequirement.class);
+ }
+ if (dseRequirement == null) {
+ dseRequirement = testClass.getAnnotation(DseRequirement.class);
+ }
+ if (backendRequirement == null) {
+ backendRequirement = testClass.getAnnotation(BackendRequirement.class);
+ }
+ if (backendRequirements == null) {
+ backendRequirements = testClass.getAnnotation(BackendRequirements.class);
+ }
+ }
+
// build list of required versions
Collection requirements = new ArrayList<>();
if (cassandraRequirement != null) {
@@ -134,9 +174,36 @@ public static boolean meetsAny(
return true;
}
+ // First check for exclusions (include=false)
+ // If any requirement explicitly excludes the current backend, skip the test
+ boolean isExcluded =
+ requirements.stream()
+ .anyMatch(
+ requirement ->
+ !requirement.include && requirement.getBackendType() == configuredBackend);
+ if (isExcluded) {
+ return false;
+ }
+
+ // Check if there are any inclusion requirements
+ boolean hasInclusionRequirements =
+ requirements.stream().anyMatch(requirement -> requirement.include);
+
+ // If there are no inclusion requirements (only exclusions), and we're not excluded, pass
+ if (!hasInclusionRequirements) {
+ return true;
+ }
+
+ // Then check for inclusions (include=true)
+ // At least one requirement must match the backend type and version
return requirements.stream()
.anyMatch(
requirement -> {
+ // Skip exclusion requirements
+ if (!requirement.include) {
+ return false;
+ }
+
// requirement is different db type
if (requirement.getBackendType() != configuredBackend) {
return false;
diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/session/SessionRule.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/session/SessionRule.java
index 3b792374769..c95389daaba 100644
--- a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/session/SessionRule.java
+++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/session/SessionRule.java
@@ -28,6 +28,7 @@
import com.datastax.oss.driver.api.core.metadata.schema.SchemaChangeListener;
import com.datastax.oss.driver.api.core.session.Session;
import com.datastax.oss.driver.api.testinfra.CassandraResourceRule;
+import com.datastax.oss.driver.api.testinfra.astra.BaseAstraRule;
import com.datastax.oss.driver.api.testinfra.ccm.BaseCcmRule;
import com.datastax.oss.driver.api.testinfra.ccm.CcmBridge;
import com.datastax.oss.driver.api.testinfra.ccm.SchemaChangeSynchronizer;
@@ -35,6 +36,8 @@
import com.datastax.oss.driver.api.testinfra.simulacron.SimulacronRule;
import java.util.Objects;
import org.junit.rules.ExternalResource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* Creates and manages a {@link Session} instance for a test.
@@ -63,6 +66,7 @@
*/
public class SessionRule extends ExternalResource {
+ private static final Logger LOG = LoggerFactory.getLogger(SessionRule.class);
private static final Version V6_8_0 = Objects.requireNonNull(Version.parse("6.8.0"));
// the CCM or Simulacron rule to depend on
@@ -100,10 +104,22 @@ public SessionRule(
this.cassandraResource = cassandraResource;
this.nodeStateListener = nodeStateListener;
this.schemaChangeListener = schemaChangeListener;
- this.keyspace =
- (cassandraResource instanceof SimulacronRule || !createKeyspace)
- ? null
- : SessionUtils.uniqueKeyspaceId();
+ // Determine keyspace based on backend type:
+ // - Simulacron: no keyspace (null)
+ // - Astra: use shared keyspace from AstraBridge (when createKeyspace is true)
+ // - CCM/other: generate unique keyspace (when createKeyspace is true)
+ // - When createKeyspace is false: no keyspace (null)
+ if (!createKeyspace || cassandraResource instanceof SimulacronRule) {
+ this.keyspace = null;
+ } else if (cassandraResource instanceof BaseAstraRule) {
+ // For Astra, use the shared keyspace from AstraBridge
+ BaseAstraRule astraRule = (BaseAstraRule) cassandraResource;
+ String sharedKeyspace = astraRule.getAstraBridge().getKeyspace();
+ this.keyspace = sharedKeyspace != null ? CqlIdentifier.fromCql(sharedKeyspace) : null;
+ } else {
+ // For CCM and other backends, generate a unique keyspace
+ this.keyspace = SessionUtils.uniqueKeyspaceId();
+ }
this.configLoader = configLoader;
this.graphName = graphName;
this.isCoreGraph = isCoreGraph;
@@ -144,12 +160,32 @@ public SessionRule(
@Override
protected void before() {
+ // Create session without keyspace first
session =
SessionUtils.newSession(
cassandraResource, null, nodeStateListener, schemaChangeListener, null, configLoader);
+
slowProfile = SessionUtils.slowProfile(session);
+
+ // Create keyspace if needed
if (keyspace != null) {
- SessionUtils.createKeyspace(session, keyspace, slowProfile);
+ if (cassandraResource instanceof BaseAstraRule) {
+ // For Astra, the shared keyspace already exists - just switch to it
+ BaseAstraRule astraRule = (BaseAstraRule) cassandraResource;
+ String sharedKeyspace = astraRule.getAstraBridge().getKeyspace();
+ LOG.warn(
+ "Using shared Astra keyspace: {} with CassandraResource: {}",
+ sharedKeyspace,
+ cassandraResource.getClass().getSimpleName());
+ } else {
+ // For CCM and other backends, create a unique keyspace using CQL
+ LOG.warn(
+ "Creating keyspace: {} with CassandraResource: {}",
+ keyspace,
+ cassandraResource.getClass().getSimpleName());
+ SessionUtils.createKeyspace(session, keyspace, slowProfile);
+ }
+ // Switch to the keyspace
session.execute(
SimpleStatement.newInstance(String.format("USE %s", keyspace.asCql(false))),
Statement.SYNC);
@@ -194,7 +230,10 @@ protected void after() {
.setSystemQuery(true),
ScriptGraphStatement.SYNC);
}
- if (keyspace != null) {
+ // Only drop keyspace for non-Astra resources (Astra keyspaces are managed by Astra)
+ if (keyspace != null
+ && !(cassandraResource
+ instanceof com.datastax.oss.driver.api.testinfra.astra.BaseAstraRule)) {
SchemaChangeSynchronizer.withLock(
() -> {
SessionUtils.dropKeyspace(session, keyspace, slowProfile);
diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/session/SessionUtils.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/session/SessionUtils.java
index 7536c0ffdc0..4eef9948496 100644
--- a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/session/SessionUtils.java
+++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/session/SessionUtils.java
@@ -31,7 +31,9 @@
import com.datastax.oss.driver.api.core.session.Session;
import com.datastax.oss.driver.api.core.session.SessionBuilder;
import com.datastax.oss.driver.api.testinfra.CassandraResourceRule;
+import com.datastax.oss.driver.api.testinfra.astra.BaseAstraRule;
import com.datastax.oss.driver.internal.core.loadbalancing.helper.NodeFilterToDistanceEvaluatorAdapter;
+import java.io.File;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
@@ -140,6 +142,21 @@ public static SessionT newSession(
return newSession(cassandraResourceRule, keyspace, null, null, null, loader);
}
+ /**
+ * Returns a SessionBuilder configured for the given CassandraResourceRule.
+ *
+ * This method handles the differences between Astra (which uses Secure Connect Bundle) and
+ * other backends (which use contact points).
+ *
+ * @param cassandraResource the Cassandra resource to connect to
+ * @param keyspace the keyspace to connect to (can be null)
+ * @return a SessionBuilder configured for the given resource
+ */
+ public static SessionBuilder, SessionT> baseBuilder(
+ CassandraResourceRule cassandraResource, CqlIdentifier keyspace) {
+ return builder(cassandraResource, keyspace, null, null, null);
+ }
+
private static SessionBuilder, SessionT> builder(
CassandraResourceRule cassandraResource,
CqlIdentifier keyspace,
@@ -147,8 +164,31 @@ private static SessionBuilder, SessionT> builder(
SchemaChangeListener schemaChangeListener,
Predicate nodeFilter) {
SessionBuilder, SessionT> builder = baseBuilder();
+
+ // Check if this is an Astra resource - use Secure Connect Bundle instead of contact points
+ if (cassandraResource instanceof BaseAstraRule) {
+ BaseAstraRule astraRule = (BaseAstraRule) cassandraResource;
+ File secureConnectBundle = astraRule.getSecureConnectBundle();
+ if (secureConnectBundle != null) {
+ builder.withCloudSecureConnectBundle(secureConnectBundle.toPath());
+
+ // Add authentication credentials for Astra using token
+ // For Astra, username is "token" and password is the actual token value
+ String token = astraRule.getAstraBridge().getToken();
+ if (token != null) {
+ builder.withAuthCredentials("token", token);
+ }
+ } else {
+ throw new IllegalStateException(
+ "Astra Secure Connect Bundle is not available. "
+ + "Make sure the AstraRule has been initialized.");
+ }
+ } else {
+ // For non-Astra resources, use contact points
+ builder.addContactEndPoints(cassandraResource.getContactPoints());
+ }
+
builder
- .addContactEndPoints(cassandraResource.getContactPoints())
.withKeyspace(keyspace)
.withNodeStateListener(nodeStateListener)
.withSchemaChangeListener(schemaChangeListener);
diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/simulacron/SimulacronRule.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/simulacron/SimulacronRule.java
index d958d097a5d..93fb4c22857 100644
--- a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/simulacron/SimulacronRule.java
+++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/simulacron/SimulacronRule.java
@@ -19,8 +19,10 @@
import com.datastax.oss.driver.api.core.DefaultProtocolVersion;
import com.datastax.oss.driver.api.core.ProtocolVersion;
+import com.datastax.oss.driver.api.core.Version;
import com.datastax.oss.driver.api.core.metadata.EndPoint;
import com.datastax.oss.driver.api.testinfra.CassandraResourceRule;
+import com.datastax.oss.driver.api.testinfra.requirement.BackendType;
import com.datastax.oss.driver.internal.core.metadata.DefaultEndPoint;
import com.datastax.oss.simulacron.common.cluster.ClusterSpec;
import com.datastax.oss.simulacron.server.BoundCluster;
@@ -95,4 +97,27 @@ public Set getContactPoints() {
public ProtocolVersion getHighestProtocolVersion() {
return DefaultProtocolVersion.V4;
}
+
+ @Override
+ public BackendType getDistribution() {
+ // Simulacron simulates Cassandra
+ return BackendType.CASSANDRA;
+ }
+
+ @Override
+ public boolean isDistributionOf(BackendType type) {
+ return type == BackendType.CASSANDRA;
+ }
+
+ @Override
+ public Version getDistributionVersion() {
+ // Simulacron simulates Cassandra 4.0
+ return Version.parse("4.0.0");
+ }
+
+ @Override
+ public Version getCassandraVersion() {
+ // Simulacron simulates Cassandra 4.0
+ return Version.parse("4.0.0");
+ }
}