diff --git a/Jenkinsfile-astra b/Jenkinsfile-astra new file mode 100644 index 00000000000..1156a10a0fb --- /dev/null +++ b/Jenkinsfile-astra @@ -0,0 +1,179 @@ +#!groovy +/* + * 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. + */ + +def initializeEnvironment() { + env.DRIVER_DISPLAY_NAME = 'Java Driver for Apache Cassandra® - Astra Tests' + env.DRIVER_METRIC_TYPE = 'oss' + + env.GIT_SHA = "${env.GIT_COMMIT.take(7)}" + env.GITHUB_PROJECT_URL = "https://${GIT_URL.replaceFirst(/(git@|http:\/\/|https:\/\/)/, '').replace(':', '/').replace('.git', '')}" + env.GITHUB_BRANCH_URL = "${GITHUB_PROJECT_URL}/tree/${env.BRANCH_NAME}" + env.GITHUB_COMMIT_URL = "${GITHUB_PROJECT_URL}/commit/${env.GIT_COMMIT}" + + env.MAVEN_HOME = "${env.HOME}/.mvn/apache-maven-3.8.8" + env.PATH = "${env.MAVEN_HOME}/bin:/home/jenkins/.astra/cli:${env.PATH}" + + env.JAVA_HOME = sh(label: 'Get JAVA_HOME',script: '''#!/bin/bash -le + . ${JABBA_SHELL} + jabba which 1.8''', returnStdout: true).trim() + + sh label: 'Display Java and environment information',script: '''#!/bin/bash -le + . ${JABBA_SHELL} + jabba use 1.8 + + java -version + mvn -v + printenv | sort + astra help + ''' +} + +def buildDriver() { + sh label: 'Build driver', script: '''#!/bin/bash -le + . ${JABBA_SHELL} + jabba use 1.8 + + echo "Building with Java version 1.8" + + mvn -B -V install -DskipTests -Dmaven.javadoc.skip=true + ''' +} + +def executeAstraTests() { + withCredentials([ + string(credentialsId: 'astra-integration-test-token', variable: 'ASTRA_TOKEN') + ]) { + sh label: 'Execute Astra integration tests', script: '''#!/bin/bash -lex + . ${JABBA_SHELL} + jabba use 1.8 + + printenv | sort + + mvn -B -V verify \ + -DfailIfNoTests=false \ + -Dmaven.test.failure.ignore=true \ + -Dmaven.javadoc.skip=true \ + -Dccm.distribution=Astra \ + -Dastra.token=${ASTRA_TOKEN} + ''' + } +} + +def notifySlack(status = 'started') { + def color = 'good' // Green + if (status.equalsIgnoreCase('aborted')) { + color = '808080' // Grey + } else if (status.equalsIgnoreCase('unstable')) { + color = 'warning' // Orange + } else if (status.equalsIgnoreCase('failed')) { + color = 'danger' // Red + } + + def message = """Build ${status} for ${env.DRIVER_DISPLAY_NAME} +<${env.GITHUB_BRANCH_URL}|${env.BRANCH_NAME}> - <${env.RUN_DISPLAY_URL}|#${env.BUILD_NUMBER}> - <${env.GITHUB_COMMIT_URL}|${env.GIT_SHA}>""" + if (!status.equalsIgnoreCase('Started')) { + message += """ +${status} after ${currentBuild.durationString - ' and counting'}""" + } + + slackSend color: "${color}", + channel: "#java-driver-dev-bots", + message: "${message}" +} + +pipeline { + agent { + label 'ubuntu/jammy64/java-driver' + } + + // Global pipeline timeout + options { + timeout(time: 2, unit: 'HOURS') + buildDiscarder(logRotator(artifactNumToKeepStr: '10', // Keep only the last 10 artifacts + numToKeepStr: '50')) // Keep only the last 50 build records + } + + environment { + JABBA_SHELL = '/usr/lib/jabba/jabba.sh' + SERIAL_ITS_ARGUMENT = "-DskipSerialITs=${params.SKIP_SERIAL_ITS}" + ISOLATED_ITS_ARGUMENT = "-DskipIsolatedITs=${params.SKIP_ISOLATED_ITS}" + PARALLELIZABLE_ITS_ARGUMENT = "-DskipParallelizableITs=${params.SKIP_PARALLELIZABLE_ITS}" + INTEGRATION_TESTS_FILTER = "${params.INTEGRATION_TESTS_FILTER}" + } + + stages { + stage('Initialize-Environment') { + steps { + initializeEnvironment() + notifySlack() + } + } + + stage('Describe-Build') { + steps { + script { + currentBuild.displayName = "Astra Integration Tests" + currentBuild.description = "Running integration tests against DataStax Astra" + } + } + } + + stage('Build-Driver') { + steps { + buildDriver() + } + } + + stage('Execute-Astra-Tests') { + steps { + catchError { + executeAstraTests() + } + } + post { + always { + /* + * Empty results are possible + * + * - Build failures during mvn verify may exist so report may not be available + * - With boolean parameters to skip tests a failsafe report may not be available + */ + junit testResults: '**/target/surefire-reports/TEST-*.xml', allowEmptyResults: true + junit testResults: '**/target/failsafe-reports/TEST-*.xml', allowEmptyResults: true + } + } + } + } + + post { + aborted { + notifySlack('aborted') + } + success { + notifySlack('completed') + } + unstable { + notifySlack('unstable') + } + failure { + notifySlack('FAILED') + } + } +} diff --git a/Jenkinsfile-datastax b/Jenkinsfile-datastax index 602f33101ca..9a7b7923f32 100644 --- a/Jenkinsfile-datastax +++ b/Jenkinsfile-datastax @@ -402,7 +402,7 @@ pipeline { } environment { - OS_VERSION = 'ubuntu/focal64/java-driver' + OS_VERSION = 'ubuntu/jammy64/java-driver' JABBA_SHELL = '/usr/lib/jabba/jabba.sh' CCM_ENVIRONMENT_SHELL = '/usr/local/bin/ccm_environment.sh' SERIAL_ITS_ARGUMENT = "-DskipSerialITs=${params.SKIP_SERIAL_ITS}" diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/ssl/ReloadingKeyManagerFactoryTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/ssl/ReloadingKeyManagerFactoryTest.java index d07b45c21df..e0ba675b846 100644 --- a/core/src/test/java/com/datastax/oss/driver/internal/core/ssl/ReloadingKeyManagerFactoryTest.java +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/ssl/ReloadingKeyManagerFactoryTest.java @@ -26,6 +26,7 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.SocketException; +import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -55,12 +56,22 @@ public class ReloadingKeyManagerFactoryTest { private static final Logger logger = LoggerFactory.getLogger(ReloadingKeyManagerFactoryTest.class); - static final Path CERT_BASE = - Paths.get( - ReloadingKeyManagerFactoryTest.class - .getResource( - String.format("/%s/certs/", ReloadingKeyManagerFactoryTest.class.getSimpleName())) - .getPath()); + static final Path CERT_BASE; + + static { + try { + CERT_BASE = + Paths.get( + ReloadingKeyManagerFactoryTest.class + .getResource( + String.format( + "/%s/certs/", ReloadingKeyManagerFactoryTest.class.getSimpleName())) + .toURI()); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + static final Path SERVER_KEYSTORE_PATH = CERT_BASE.resolve("server.keystore"); static final Path SERVER_TRUSTSTORE_PATH = CERT_BASE.resolve("server.truststore"); diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index e302e12077f..d500c6e7296 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -33,6 +33,9 @@ ${skipITs} ${skipITs} ${skipITs} + + classes + 8 @@ -238,8 +241,10 @@ ${testing.jvm}/bin/java com.datastax.oss.driver.categories.ParallelizableTests - classes - 8 + + ${astra.parallel.mode} + ${astra.thread.count} ${project.build.directory}/failsafe-reports/failsafe-summary-parallelized.xml ${skipParallelizableITs} ${blockhound.argline} @@ -335,4 +340,23 @@ + + + + astra-sequential + + + ccm.distribution + Astra + + + + + none + 1 + + + diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/core/PeersV2NodeRefreshIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/core/PeersV2NodeRefreshIT.java index 47f3e3957af..c1df9158a62 100644 --- a/integration-tests/src/test/java/com/datastax/oss/driver/core/PeersV2NodeRefreshIT.java +++ b/integration-tests/src/test/java/com/datastax/oss/driver/core/PeersV2NodeRefreshIT.java @@ -17,11 +17,13 @@ */ package com.datastax.oss.driver.core; +import static com.datastax.oss.driver.api.testinfra.ccm.CcmBridge.isDistributionOf; import static org.assertj.core.api.Assertions.assertThat; import com.datastax.oss.driver.api.core.CqlSession; import com.datastax.oss.driver.api.core.metadata.EndPoint; import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.testinfra.requirement.BackendType; import com.datastax.oss.driver.internal.core.context.InternalDriverContext; import com.datastax.oss.protocol.internal.request.Query; import com.datastax.oss.simulacron.common.cluster.ClusterSpec; @@ -30,6 +32,7 @@ import com.datastax.oss.simulacron.server.Server; import java.util.concurrent.ExecutionException; import org.junit.AfterClass; +import org.junit.Assume; import org.junit.BeforeClass; import org.junit.Test; @@ -41,6 +44,7 @@ public class PeersV2NodeRefreshIT { @BeforeClass public static void setup() { + Assume.assumeTrue("Skipping for Astra", !isDistributionOf(BackendType.ASTRA)); peersV2Server = Server.builder().withMultipleNodesPerIp(true).build(); cluster = peersV2Server.register(ClusterSpec.builder().withNodes(2)); } diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverExecutionProfileCcmIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverExecutionProfileCcmIT.java index 1eee9c304b6..8500f267da2 100644 --- a/integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverExecutionProfileCcmIT.java +++ b/integration-tests/src/test/java/com/datastax/oss/driver/core/config/DriverExecutionProfileCcmIT.java @@ -31,6 +31,8 @@ import com.datastax.oss.driver.api.core.cql.PreparedStatement; import com.datastax.oss.driver.api.core.cql.SimpleStatement; import com.datastax.oss.driver.api.testinfra.ccm.CcmRule; +import com.datastax.oss.driver.api.testinfra.requirement.BackendRequirement; +import com.datastax.oss.driver.api.testinfra.requirement.BackendType; import com.datastax.oss.driver.api.testinfra.session.SessionUtils; import com.datastax.oss.driver.categories.ParallelizableTests; import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; @@ -40,6 +42,7 @@ import org.junit.experimental.categories.Category; @Category(ParallelizableTests.class) +@BackendRequirement(type = BackendType.ASTRA, include = false) public class DriverExecutionProfileCcmIT { @ClassRule public static final CcmRule CCM_RULE = CcmRule.getInstance(); diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/core/cql/BoundStatementCcmIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/core/cql/BoundStatementCcmIT.java index 9e4b62cd230..d192f36301d 100644 --- a/integration-tests/src/test/java/com/datastax/oss/driver/core/cql/BoundStatementCcmIT.java +++ b/integration-tests/src/test/java/com/datastax/oss/driver/core/cql/BoundStatementCcmIT.java @@ -38,6 +38,7 @@ import com.datastax.oss.driver.api.core.cql.SimpleStatementBuilder; import com.datastax.oss.driver.api.core.cql.Statement; import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.session.SessionBuilder; import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; import com.datastax.oss.driver.api.testinfra.ccm.CcmRule; import com.datastax.oss.driver.api.testinfra.ccm.SchemaChangeSynchronizer; @@ -434,12 +435,9 @@ private static void verifyUnset( @SuppressWarnings("unchecked") private CqlSession sessionWithCustomCodec(CqlIntToStringCodec codec) { - return (CqlSession) - SessionUtils.baseBuilder() - .addContactEndPoints(ccmRule.getContactPoints()) - .withKeyspace(sessionRule.keyspace()) - .addTypeCodecs(codec) - .build(); + SessionBuilder builder = + SessionUtils.baseBuilder(ccmRule, sessionRule.keyspace()); + return (CqlSession) builder.addTypeCodecs(codec).build(); } private boolean supportsPerRequestKeyspace(CqlSession session) { diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/core/cql/PagingStateIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/core/cql/PagingStateIT.java index 6d33f35238a..dc7ea0ca94e 100644 --- a/integration-tests/src/test/java/com/datastax/oss/driver/core/cql/PagingStateIT.java +++ b/integration-tests/src/test/java/com/datastax/oss/driver/core/cql/PagingStateIT.java @@ -31,12 +31,14 @@ import com.datastax.oss.driver.api.core.type.reflect.GenericType; import com.datastax.oss.driver.api.testinfra.ccm.CcmRule; import com.datastax.oss.driver.api.testinfra.ccm.SchemaChangeSynchronizer; +import com.datastax.oss.driver.api.testinfra.requirement.BackendType; import com.datastax.oss.driver.api.testinfra.session.SessionRule; import com.datastax.oss.driver.api.testinfra.session.SessionUtils; import com.datastax.oss.driver.categories.ParallelizableTests; import com.datastax.oss.driver.internal.core.type.codec.IntCodec; import edu.umd.cs.findbugs.annotations.Nullable; import java.util.function.UnaryOperator; +import org.junit.Assume; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; @@ -108,6 +110,7 @@ private void should_extract_and_reuse(UnaryOperator transformation) @Test public void should_inject_in_simple_statement_with_custom_codecs() { + Assume.assumeFalse("Skipped for Astra", CCM_RULE.isDistributionOf(BackendType.ASTRA)); try (CqlSession session = (CqlSession) SessionUtils.baseBuilder() diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/core/cql/PreparedStatementCachingIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/core/cql/PreparedStatementCachingIT.java index 617d489fb95..ebb8bd34f16 100644 --- a/integration-tests/src/test/java/com/datastax/oss/driver/core/cql/PreparedStatementCachingIT.java +++ b/integration-tests/src/test/java/com/datastax/oss/driver/core/cql/PreparedStatementCachingIT.java @@ -28,8 +28,10 @@ import com.datastax.oss.driver.api.core.metrics.DefaultSessionMetric; import com.datastax.oss.driver.api.core.session.ProgrammaticArguments; import com.datastax.oss.driver.api.core.session.SessionBuilder; +import com.datastax.oss.driver.api.testinfra.ccm.CcmBridge; import com.datastax.oss.driver.api.testinfra.ccm.CustomCcmRule; import com.datastax.oss.driver.api.testinfra.ccm.SchemaChangeSynchronizer; +import com.datastax.oss.driver.api.testinfra.requirement.BackendType; import com.datastax.oss.driver.api.testinfra.session.SessionRule; import com.datastax.oss.driver.api.testinfra.session.SessionUtils; import com.datastax.oss.driver.categories.IsolatedTests; @@ -59,6 +61,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import org.junit.AfterClass; +import org.junit.Assume; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; @@ -177,6 +180,7 @@ protected DriverContext buildContext( @BeforeClass public static void setup() { + Assume.assumeTrue("Skipped for Astra", !CcmBridge.isDistributionOf(BackendType.ASTRA)); System.setProperty( SessionUtils.SESSION_BUILDER_CLASS_PROPERTY, PreparedStatementCachingIT.class.getName()); } diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/core/cql/QueryTraceIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/core/cql/QueryTraceIT.java index 37a600efbc4..708bc3cbd43 100644 --- a/integration-tests/src/test/java/com/datastax/oss/driver/core/cql/QueryTraceIT.java +++ b/integration-tests/src/test/java/com/datastax/oss/driver/core/cql/QueryTraceIT.java @@ -28,6 +28,7 @@ import com.datastax.oss.driver.api.core.cql.SimpleStatement; 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.BackendRequirement; import com.datastax.oss.driver.api.testinfra.requirement.BackendType; import com.datastax.oss.driver.api.testinfra.session.SessionRule; import com.datastax.oss.driver.categories.ParallelizableTests; @@ -40,6 +41,7 @@ import org.junit.rules.TestRule; @Category(ParallelizableTests.class) +@BackendRequirement(type = BackendType.ASTRA, include = false) public class QueryTraceIT { private static final CcmRule CCM_RULE = CcmRule.getInstance(); diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/core/cql/reactive/DefaultReactiveResultSetIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/core/cql/reactive/DefaultReactiveResultSetIT.java index c00cf064e51..dbc7823efb5 100644 --- a/integration-tests/src/test/java/com/datastax/oss/driver/core/cql/reactive/DefaultReactiveResultSetIT.java +++ b/integration-tests/src/test/java/com/datastax/oss/driver/core/cql/reactive/DefaultReactiveResultSetIT.java @@ -22,6 +22,7 @@ import com.datastax.dse.driver.api.core.cql.reactive.ReactiveResultSet; import com.datastax.dse.driver.api.core.cql.reactive.ReactiveRow; import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.DefaultConsistencyLevel; import com.datastax.oss.driver.api.core.config.DefaultDriverOption; import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; import com.datastax.oss.driver.api.core.cql.BatchStatement; @@ -33,6 +34,8 @@ import com.datastax.oss.driver.api.core.cql.SimpleStatement; import com.datastax.oss.driver.api.testinfra.ccm.CcmRule; import com.datastax.oss.driver.api.testinfra.ccm.SchemaChangeSynchronizer; +import com.datastax.oss.driver.api.testinfra.requirement.BackendRequirement; +import com.datastax.oss.driver.api.testinfra.requirement.BackendType; import com.datastax.oss.driver.api.testinfra.session.SessionRule; import com.datastax.oss.driver.categories.ParallelizableTests; import com.datastax.oss.driver.internal.core.cql.EmptyColumnDefinitions; @@ -54,6 +57,7 @@ @RunWith(DataProviderRunner.class) @Category(ParallelizableTests.class) +@BackendRequirement(type = BackendType.ASTRA, include = false) public class DefaultReactiveResultSetIT { private static CcmRule ccmRule = CcmRule.getInstance(); @@ -307,10 +311,27 @@ private static BatchStatement createCASBatch() { private static void partiallyDeleteInsertedRows() { CqlSession session = sessionRule.session(); - session.execute(" DELETE FROM test_reactive_write WHERE pk = 0 and cc = 5"); - session.execute(" DELETE FROM test_reactive_write WHERE pk = 0 and cc = 6"); - session.execute(" DELETE FROM test_reactive_write WHERE pk = 0 and cc = 7"); - session.execute(" DELETE FROM test_reactive_write WHERE pk = 0 and cc = 8"); - session.execute(" DELETE FROM test_reactive_write WHERE pk = 0 and cc = 9"); + // Use ALL consistency level to ensure deletes are immediately visible across all replicas + // This is important for Astra (distributed system) to avoid eventual consistency issues + session.execute( + SimpleStatement.builder("DELETE FROM test_reactive_write WHERE pk = 0 and cc = 5") + .setConsistencyLevel(DefaultConsistencyLevel.ALL) + .build()); + session.execute( + SimpleStatement.builder("DELETE FROM test_reactive_write WHERE pk = 0 and cc = 6") + .setConsistencyLevel(DefaultConsistencyLevel.ALL) + .build()); + session.execute( + SimpleStatement.builder("DELETE FROM test_reactive_write WHERE pk = 0 and cc = 7") + .setConsistencyLevel(DefaultConsistencyLevel.ALL) + .build()); + session.execute( + SimpleStatement.builder("DELETE FROM test_reactive_write WHERE pk = 0 and cc = 8") + .setConsistencyLevel(DefaultConsistencyLevel.ALL) + .build()); + session.execute( + SimpleStatement.builder("DELETE FROM test_reactive_write WHERE pk = 0 and cc = 9") + .setConsistencyLevel(DefaultConsistencyLevel.ALL) + .build()); } } diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/core/data/DataTypeIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/core/data/DataTypeIT.java index e3d891454de..2f719f3039e 100644 --- a/integration-tests/src/test/java/com/datastax/oss/driver/core/data/DataTypeIT.java +++ b/integration-tests/src/test/java/com/datastax/oss/driver/core/data/DataTypeIT.java @@ -47,6 +47,8 @@ import com.datastax.oss.driver.api.core.type.UserDefinedType; import com.datastax.oss.driver.api.core.type.codec.TypeCodec; import com.datastax.oss.driver.api.testinfra.ccm.CcmRule; +import com.datastax.oss.driver.api.testinfra.requirement.BackendRequirement; +import com.datastax.oss.driver.api.testinfra.requirement.BackendType; import com.datastax.oss.driver.api.testinfra.session.SessionRule; import com.datastax.oss.driver.categories.ParallelizableTests; import com.datastax.oss.driver.internal.core.type.DefaultListType; @@ -89,6 +91,7 @@ @Category(ParallelizableTests.class) @RunWith(DataProviderRunner.class) +@BackendRequirement(type = BackendType.ASTRA, include = false) public class DataTypeIT { private static final CcmRule CCM_RULE = CcmRule.getInstance(); diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/core/metadata/CaseSensitiveUdtIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/core/metadata/CaseSensitiveUdtIT.java index f80b02207f8..982e31692e9 100644 --- a/integration-tests/src/test/java/com/datastax/oss/driver/core/metadata/CaseSensitiveUdtIT.java +++ b/integration-tests/src/test/java/com/datastax/oss/driver/core/metadata/CaseSensitiveUdtIT.java @@ -26,6 +26,8 @@ import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata; import com.datastax.oss.driver.api.core.type.UserDefinedType; import com.datastax.oss.driver.api.testinfra.ccm.CcmRule; +import com.datastax.oss.driver.api.testinfra.requirement.BackendRequirement; +import com.datastax.oss.driver.api.testinfra.requirement.BackendType; import com.datastax.oss.driver.api.testinfra.session.SessionRule; import com.datastax.oss.driver.api.testinfra.session.SessionUtils; import com.datastax.oss.driver.categories.ParallelizableTests; @@ -46,6 +48,7 @@ * @see JAVA-2028 */ @Category(ParallelizableTests.class) +@BackendRequirement(type = BackendType.ASTRA, include = false) public class CaseSensitiveUdtIT { private static final CcmRule CCM_RULE = CcmRule.getInstance(); diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/core/metadata/DescribeIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/core/metadata/DescribeIT.java index 4d6c2a7a3b1..172fb633bb1 100644 --- a/integration-tests/src/test/java/com/datastax/oss/driver/core/metadata/DescribeIT.java +++ b/integration-tests/src/test/java/com/datastax/oss/driver/core/metadata/DescribeIT.java @@ -29,6 +29,7 @@ import com.datastax.oss.driver.api.core.metadata.schema.TableMetadata; import com.datastax.oss.driver.api.testinfra.ccm.CcmRule; import com.datastax.oss.driver.api.testinfra.ccm.SchemaChangeSynchronizer; +import com.datastax.oss.driver.api.testinfra.requirement.BackendRequirement; import com.datastax.oss.driver.api.testinfra.requirement.BackendType; import com.datastax.oss.driver.api.testinfra.session.SessionRule; import com.datastax.oss.driver.api.testinfra.session.SessionUtils; @@ -58,6 +59,7 @@ import org.slf4j.LoggerFactory; @Category(ParallelizableTests.class) +@BackendRequirement(type = BackendType.ASTRA, include = false) public class DescribeIT { private static final Logger LOG = LoggerFactory.getLogger(DescribeIT.class); diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/core/metadata/MetadataIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/core/metadata/MetadataIT.java index 1b1aed4b3de..422ee5217f9 100644 --- a/integration-tests/src/test/java/com/datastax/oss/driver/core/metadata/MetadataIT.java +++ b/integration-tests/src/test/java/com/datastax/oss/driver/core/metadata/MetadataIT.java @@ -23,6 +23,8 @@ import com.datastax.oss.driver.api.core.metadata.Metadata; import com.datastax.oss.driver.api.testinfra.ccm.CcmBridge; import com.datastax.oss.driver.api.testinfra.ccm.CcmRule; +import com.datastax.oss.driver.api.testinfra.requirement.BackendRequirement; +import com.datastax.oss.driver.api.testinfra.requirement.BackendType; import com.datastax.oss.driver.api.testinfra.session.SessionRule; import com.datastax.oss.driver.categories.ParallelizableTests; import org.junit.Rule; @@ -32,6 +34,7 @@ import org.junit.rules.TestRule; @Category(ParallelizableTests.class) +@BackendRequirement(type = BackendType.ASTRA, include = false) public class MetadataIT { private CcmRule ccmRule = CcmRule.getInstance(); diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/core/metadata/NodeMetadataIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/core/metadata/NodeMetadataIT.java index 8f5680ff41a..a58f69e8bbf 100644 --- a/integration-tests/src/test/java/com/datastax/oss/driver/core/metadata/NodeMetadataIT.java +++ b/integration-tests/src/test/java/com/datastax/oss/driver/core/metadata/NodeMetadataIT.java @@ -43,6 +43,7 @@ import org.junit.experimental.categories.Category; @Category(ParallelizableTests.class) +@BackendRequirement(type = BackendType.ASTRA, include = false) public class NodeMetadataIT { @Rule public CcmRule ccmRule = CcmRule.getInstance(); diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/core/metadata/SchemaIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/core/metadata/SchemaIT.java index df5571974c1..0cc1debf8ec 100644 --- a/integration-tests/src/test/java/com/datastax/oss/driver/core/metadata/SchemaIT.java +++ b/integration-tests/src/test/java/com/datastax/oss/driver/core/metadata/SchemaIT.java @@ -77,6 +77,7 @@ public void should_not_expose_system_and_test_keyspace() { } @Test + @BackendRequirement(type = BackendType.ASTRA, include = false) public void should_expose_test_keyspace() { Map keyspaces = sessionRule.session().getMetadata().getKeyspaces(); @@ -84,6 +85,7 @@ public void should_expose_test_keyspace() { } @Test + @BackendRequirement(type = BackendType.ASTRA, include = false) public void should_filter_by_keyspaces() { DriverConfigLoader loader = SessionUtils.configLoaderBuilder() diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/core/session/RequestProcessorIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/core/session/RequestProcessorIT.java index e2b3caeb1f4..aafdbea7605 100644 --- a/integration-tests/src/test/java/com/datastax/oss/driver/core/session/RequestProcessorIT.java +++ b/integration-tests/src/test/java/com/datastax/oss/driver/core/session/RequestProcessorIT.java @@ -26,6 +26,8 @@ import com.datastax.oss.driver.api.core.cql.PreparedStatement; import com.datastax.oss.driver.api.core.cql.SimpleStatement; import com.datastax.oss.driver.api.testinfra.ccm.CcmRule; +import com.datastax.oss.driver.api.testinfra.requirement.BackendRequirement; +import com.datastax.oss.driver.api.testinfra.requirement.BackendType; import com.datastax.oss.driver.api.testinfra.session.SessionRule; import com.datastax.oss.driver.categories.ParallelizableTests; import com.datastax.oss.driver.example.guava.api.GuavaSession; @@ -64,6 +66,7 @@ * simplifies a certain query down to 1 parameter. */ @Category(ParallelizableTests.class) +@BackendRequirement(type = BackendType.ASTRA, include = false) public class RequestProcessorIT { private static final CcmRule CCM_RULE = CcmRule.getInstance(); diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/core/tracker/RequestIdGeneratorIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/core/tracker/RequestIdGeneratorIT.java index 516a62bb1f7..6946e858ac6 100644 --- a/integration-tests/src/test/java/com/datastax/oss/driver/core/tracker/RequestIdGeneratorIT.java +++ b/integration-tests/src/test/java/com/datastax/oss/driver/core/tracker/RequestIdGeneratorIT.java @@ -29,6 +29,8 @@ import com.datastax.oss.driver.api.core.session.Request; import com.datastax.oss.driver.api.core.tracker.RequestIdGenerator; import com.datastax.oss.driver.api.testinfra.ccm.CcmRule; +import com.datastax.oss.driver.api.testinfra.requirement.BackendRequirement; +import com.datastax.oss.driver.api.testinfra.requirement.BackendType; import com.datastax.oss.driver.api.testinfra.session.SessionUtils; import com.datastax.oss.driver.categories.ParallelizableTests; import com.datastax.oss.protocol.internal.util.collection.NullAllowingImmutableMap; @@ -43,6 +45,7 @@ import org.junit.rules.TestRule; @Category(ParallelizableTests.class) +@BackendRequirement(type = BackendType.ASTRA, include = false) // TODO: Run with Astra public class RequestIdGeneratorIT { private CcmRule ccmRule = CcmRule.getInstance(); diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/core/type/codec/registry/CodecRegistryIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/core/type/codec/registry/CodecRegistryIT.java index 74472e8bab9..4c746a2d649 100644 --- a/integration-tests/src/test/java/com/datastax/oss/driver/core/type/codec/registry/CodecRegistryIT.java +++ b/integration-tests/src/test/java/com/datastax/oss/driver/core/type/codec/registry/CodecRegistryIT.java @@ -39,6 +39,8 @@ import com.datastax.oss.driver.api.core.type.reflect.GenericType; import com.datastax.oss.driver.api.testinfra.ccm.CcmRule; import com.datastax.oss.driver.api.testinfra.ccm.SchemaChangeSynchronizer; +import com.datastax.oss.driver.api.testinfra.requirement.BackendRequirement; +import com.datastax.oss.driver.api.testinfra.requirement.BackendType; import com.datastax.oss.driver.api.testinfra.session.SessionRule; import com.datastax.oss.driver.api.testinfra.session.SessionUtils; import com.datastax.oss.driver.categories.ParallelizableTests; @@ -66,6 +68,7 @@ import org.junit.rules.TestRule; @Category(ParallelizableTests.class) +@BackendRequirement(type = BackendType.ASTRA, include = false) public class CodecRegistryIT { private static final CcmRule CCM_RULE = CcmRule.getInstance(); diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/mapper/DefaultKeyspaceIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/mapper/DefaultKeyspaceIT.java index 30a808e87a9..08eff73d861 100644 --- a/integration-tests/src/test/java/com/datastax/oss/driver/mapper/DefaultKeyspaceIT.java +++ b/integration-tests/src/test/java/com/datastax/oss/driver/mapper/DefaultKeyspaceIT.java @@ -42,6 +42,8 @@ import com.datastax.oss.driver.api.mapper.annotations.Update; import com.datastax.oss.driver.api.mapper.entity.saving.NullSavingStrategy; import com.datastax.oss.driver.api.testinfra.ccm.CcmRule; +import com.datastax.oss.driver.api.testinfra.requirement.BackendRequirement; +import com.datastax.oss.driver.api.testinfra.requirement.BackendType; import com.datastax.oss.driver.api.testinfra.session.SessionRule; import com.datastax.oss.driver.categories.ParallelizableTests; import java.util.Objects; @@ -54,6 +56,7 @@ import org.junit.rules.TestRule; @Category(ParallelizableTests.class) +@BackendRequirement(type = BackendType.ASTRA, include = false) public class DefaultKeyspaceIT { private static final String DEFAULT_KEYSPACE = "default_keyspace"; private static final CcmRule CCM_RULE = CcmRule.getInstance(); diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/mapper/QueryKeyspaceAndTableIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/mapper/QueryKeyspaceAndTableIT.java index 9391c0363f8..7a5a598d796 100644 --- a/integration-tests/src/test/java/com/datastax/oss/driver/mapper/QueryKeyspaceAndTableIT.java +++ b/integration-tests/src/test/java/com/datastax/oss/driver/mapper/QueryKeyspaceAndTableIT.java @@ -33,6 +33,8 @@ import com.datastax.oss.driver.api.mapper.annotations.Query; import com.datastax.oss.driver.api.mapper.entity.saving.NullSavingStrategy; import com.datastax.oss.driver.api.testinfra.ccm.CcmRule; +import com.datastax.oss.driver.api.testinfra.requirement.BackendRequirement; +import com.datastax.oss.driver.api.testinfra.requirement.BackendType; import com.datastax.oss.driver.api.testinfra.session.SessionRule; import com.datastax.oss.driver.categories.ParallelizableTests; import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; @@ -45,6 +47,7 @@ /** Covers the keyspace and table placeholders in {@link Query} methods. */ @Category(ParallelizableTests.class) +@BackendRequirement(type = BackendType.ASTRA, include = false) public class QueryKeyspaceAndTableIT { private static final CcmRule CCM_RULE = CcmRule.getInstance(); diff --git a/integration-tests/src/test/resources/logback-test.xml b/integration-tests/src/test/resources/logback-test.xml index a2179e4357b..745d4003f84 100644 --- a/integration-tests/src/test/resources/logback-test.xml +++ b/integration-tests/src/test/resources/logback-test.xml @@ -24,14 +24,15 @@ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - - - - - + + + + + + + + + + + diff --git a/osgi-tests/src/test/java/com/datastax/oss/driver/internal/osgi/OsgiCustomLoadBalancingPolicyIT.java b/osgi-tests/src/test/java/com/datastax/oss/driver/internal/osgi/OsgiCustomLoadBalancingPolicyIT.java index 99bd7294934..982b013bad2 100644 --- a/osgi-tests/src/test/java/com/datastax/oss/driver/internal/osgi/OsgiCustomLoadBalancingPolicyIT.java +++ b/osgi-tests/src/test/java/com/datastax/oss/driver/internal/osgi/OsgiCustomLoadBalancingPolicyIT.java @@ -19,6 +19,8 @@ import com.datastax.oss.driver.api.osgi.service.MailboxService; import com.datastax.oss.driver.api.testinfra.loadbalancing.SortingLoadBalancingPolicy; +import com.datastax.oss.driver.api.testinfra.requirement.BackendRequirement; +import com.datastax.oss.driver.api.testinfra.requirement.BackendType; import com.datastax.oss.driver.internal.osgi.checks.DefaultServiceChecks; import com.datastax.oss.driver.internal.osgi.support.BundleOptions; import com.datastax.oss.driver.internal.osgi.support.CcmExamReactorFactory; @@ -38,6 +40,7 @@ */ @RunWith(CcmPaxExam.class) @ExamReactorStrategy(CcmExamReactorFactory.class) +@BackendRequirement(type = BackendType.ASTRA, include = false) public class OsgiCustomLoadBalancingPolicyIT { @Inject MailboxService service; diff --git a/osgi-tests/src/test/java/com/datastax/oss/driver/internal/osgi/OsgiDefaultIT.java b/osgi-tests/src/test/java/com/datastax/oss/driver/internal/osgi/OsgiDefaultIT.java index a4dec25d96f..26b76c1bbeb 100644 --- a/osgi-tests/src/test/java/com/datastax/oss/driver/internal/osgi/OsgiDefaultIT.java +++ b/osgi-tests/src/test/java/com/datastax/oss/driver/internal/osgi/OsgiDefaultIT.java @@ -18,6 +18,8 @@ package com.datastax.oss.driver.internal.osgi; import com.datastax.oss.driver.api.osgi.service.MailboxService; +import com.datastax.oss.driver.api.testinfra.requirement.BackendRequirement; +import com.datastax.oss.driver.api.testinfra.requirement.BackendType; import com.datastax.oss.driver.internal.osgi.checks.DefaultServiceChecks; import com.datastax.oss.driver.internal.osgi.support.BundleOptions; import com.datastax.oss.driver.internal.osgi.support.CcmExamReactorFactory; @@ -32,6 +34,7 @@ @RunWith(CcmPaxExam.class) @ExamReactorStrategy(CcmExamReactorFactory.class) +@BackendRequirement(type = BackendType.ASTRA, include = false) public class OsgiDefaultIT { @Inject MailboxService service; diff --git a/osgi-tests/src/test/java/com/datastax/oss/driver/internal/osgi/OsgiLz4IT.java b/osgi-tests/src/test/java/com/datastax/oss/driver/internal/osgi/OsgiLz4IT.java index e8f470d3fdc..f97289443f7 100644 --- a/osgi-tests/src/test/java/com/datastax/oss/driver/internal/osgi/OsgiLz4IT.java +++ b/osgi-tests/src/test/java/com/datastax/oss/driver/internal/osgi/OsgiLz4IT.java @@ -18,6 +18,8 @@ package com.datastax.oss.driver.internal.osgi; import com.datastax.oss.driver.api.osgi.service.MailboxService; +import com.datastax.oss.driver.api.testinfra.requirement.BackendRequirement; +import com.datastax.oss.driver.api.testinfra.requirement.BackendType; import com.datastax.oss.driver.internal.osgi.checks.DefaultServiceChecks; import com.datastax.oss.driver.internal.osgi.support.BundleOptions; import com.datastax.oss.driver.internal.osgi.support.CcmExamReactorFactory; @@ -32,6 +34,7 @@ @RunWith(CcmPaxExam.class) @ExamReactorStrategy(CcmExamReactorFactory.class) +@BackendRequirement(type = BackendType.ASTRA, include = false) public class OsgiLz4IT { @Inject MailboxService service; diff --git a/osgi-tests/src/test/java/com/datastax/oss/driver/internal/osgi/OsgiReactiveIT.java b/osgi-tests/src/test/java/com/datastax/oss/driver/internal/osgi/OsgiReactiveIT.java index 1710414b67d..51dda5adfd7 100644 --- a/osgi-tests/src/test/java/com/datastax/oss/driver/internal/osgi/OsgiReactiveIT.java +++ b/osgi-tests/src/test/java/com/datastax/oss/driver/internal/osgi/OsgiReactiveIT.java @@ -21,6 +21,8 @@ import com.datastax.oss.driver.api.osgi.service.MailboxService; import com.datastax.oss.driver.api.osgi.service.reactive.ReactiveMailboxService; +import com.datastax.oss.driver.api.testinfra.requirement.BackendRequirement; +import com.datastax.oss.driver.api.testinfra.requirement.BackendType; import com.datastax.oss.driver.internal.osgi.checks.DefaultServiceChecks; import com.datastax.oss.driver.internal.osgi.checks.ReactiveServiceChecks; import com.datastax.oss.driver.internal.osgi.support.BundleOptions; @@ -36,6 +38,7 @@ @RunWith(CcmPaxExam.class) @ExamReactorStrategy(CcmExamReactorFactory.class) +@BackendRequirement(type = BackendType.ASTRA, include = false) public class OsgiReactiveIT { @Inject MailboxService service; diff --git a/osgi-tests/src/test/java/com/datastax/oss/driver/internal/osgi/OsgiShadedIT.java b/osgi-tests/src/test/java/com/datastax/oss/driver/internal/osgi/OsgiShadedIT.java index 780ed30874d..36754f6808c 100644 --- a/osgi-tests/src/test/java/com/datastax/oss/driver/internal/osgi/OsgiShadedIT.java +++ b/osgi-tests/src/test/java/com/datastax/oss/driver/internal/osgi/OsgiShadedIT.java @@ -18,6 +18,8 @@ package com.datastax.oss.driver.internal.osgi; import com.datastax.oss.driver.api.osgi.service.MailboxService; +import com.datastax.oss.driver.api.testinfra.requirement.BackendRequirement; +import com.datastax.oss.driver.api.testinfra.requirement.BackendType; import com.datastax.oss.driver.internal.osgi.checks.DefaultServiceChecks; import com.datastax.oss.driver.internal.osgi.support.BundleOptions; import com.datastax.oss.driver.internal.osgi.support.CcmExamReactorFactory; @@ -32,6 +34,7 @@ @RunWith(CcmPaxExam.class) @ExamReactorStrategy(CcmExamReactorFactory.class) +@BackendRequirement(type = BackendType.ASTRA, include = false) public class OsgiShadedIT { @Inject MailboxService service; diff --git a/test-infra/revapi.json b/test-infra/revapi.json index a16d06d75da..bf1e9a66ff4 100644 --- a/test-infra/revapi.json +++ b/test-infra/revapi.json @@ -17,6 +17,240 @@ } }, "ignore": [ + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Set com.datastax.oss.driver.api.testinfra.CassandraResourceRule::getContactPoints()", + "new": "method java.util.Set com.datastax.oss.driver.api.testinfra.CassandraResourceRule::getContactPoints()", + "justification": "JAVA-2165: Abstract node connection information" + }, + { + "code": "java.method.numberOfParametersChanged", + "old": "method void com.datastax.oss.driver.api.testinfra.loadbalancing.SortingLoadBalancingPolicy::init(java.util.Map, com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy.DistanceReporter, java.util.Set)", + "new": "method void com.datastax.oss.driver.api.testinfra.loadbalancing.SortingLoadBalancingPolicy::init(java.util.Map, com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy.DistanceReporter)", + "justification": "JAVA-2165: Abstract node connection information" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Set com.datastax.oss.driver.api.testinfra.simulacron.SimulacronRule::getContactPoints()", + "new": "method java.util.Set com.datastax.oss.driver.api.testinfra.simulacron.SimulacronRule::getContactPoints()", + "justification": "JAVA-2165: Abstract node connection information" + }, + { + "code": "java.method.returnTypeChanged", + "old": "method com.datastax.oss.driver.internal.core.config.typesafe.DefaultDriverConfigLoaderBuilder com.datastax.oss.driver.api.testinfra.session.SessionUtils::configLoaderBuilder()", + "new": "method com.datastax.oss.driver.api.core.config.ProgrammaticDriverConfigLoaderBuilder com.datastax.oss.driver.api.testinfra.session.SessionUtils::configLoaderBuilder()", + "justification": "JAVA-2201: Expose a public API for programmatic config" + }, + { + "code": "java.annotation.removed", + "old": "parameter java.util.Queue com.datastax.oss.driver.api.testinfra.loadbalancing.SortingLoadBalancingPolicy::newQueryPlan(===com.datastax.oss.driver.api.core.session.Request===, com.datastax.oss.driver.api.core.session.Session)", + "new": "parameter java.util.Queue com.datastax.oss.driver.api.testinfra.loadbalancing.SortingLoadBalancingPolicy::newQueryPlan(===com.datastax.oss.driver.api.core.session.Request===, com.datastax.oss.driver.api.core.session.Session)", + "annotation": "@edu.umd.cs.findbugs.annotations.NonNull", + "justification": "Method arguments were mistakenly annotated with @NonNull" + }, + { + "code": "java.annotation.added", + "old": "parameter java.util.Queue com.datastax.oss.driver.api.testinfra.loadbalancing.SortingLoadBalancingPolicy::newQueryPlan(===com.datastax.oss.driver.api.core.session.Request===, com.datastax.oss.driver.api.core.session.Session)", + "new": "parameter java.util.Queue com.datastax.oss.driver.api.testinfra.loadbalancing.SortingLoadBalancingPolicy::newQueryPlan(===com.datastax.oss.driver.api.core.session.Request===, com.datastax.oss.driver.api.core.session.Session)", + "annotation": "@edu.umd.cs.findbugs.annotations.Nullable", + "justification": "Method arguments were mistakenly annotated with @NonNull" + }, + { + "code": "java.annotation.removed", + "old": "parameter java.util.Queue com.datastax.oss.driver.api.testinfra.loadbalancing.SortingLoadBalancingPolicy::newQueryPlan(com.datastax.oss.driver.api.core.session.Request, ===com.datastax.oss.driver.api.core.session.Session===)", + "new": "parameter java.util.Queue com.datastax.oss.driver.api.testinfra.loadbalancing.SortingLoadBalancingPolicy::newQueryPlan(com.datastax.oss.driver.api.core.session.Request, ===com.datastax.oss.driver.api.core.session.Session===)", + "annotation": "@edu.umd.cs.findbugs.annotations.NonNull", + "justification": "Method arguments were mistakenly annotated with @NonNull" + }, + { + "code": "java.annotation.added", + "old": "parameter java.util.Queue com.datastax.oss.driver.api.testinfra.loadbalancing.SortingLoadBalancingPolicy::newQueryPlan(com.datastax.oss.driver.api.core.session.Request, ===com.datastax.oss.driver.api.core.session.Session===)", + "new": "parameter java.util.Queue com.datastax.oss.driver.api.testinfra.loadbalancing.SortingLoadBalancingPolicy::newQueryPlan(com.datastax.oss.driver.api.core.session.Request, ===com.datastax.oss.driver.api.core.session.Session===)", + "annotation": "@edu.umd.cs.findbugs.annotations.Nullable", + "justification": "Method arguments were mistakenly annotated with @NonNull" + }, + { + "code": "java.method.parameterTypeParameterChanged", + "old": "parameter com.datastax.oss.driver.api.testinfra.simulacron.QueryCounter.QueryCounterBuilder com.datastax.oss.driver.api.testinfra.simulacron.QueryCounter::builder(===com.datastax.oss.simulacron.server.BoundTopic===)", + "new": "parameter com.datastax.oss.driver.api.testinfra.simulacron.QueryCounter.QueryCounterBuilder com.datastax.oss.driver.api.testinfra.simulacron.QueryCounter::builder(===com.datastax.oss.simulacron.server.BoundTopic===)", + "justification": "Fix usage of raw type BoundTopic" + }, + { + "code": "java.field.constantValueChanged", + "old": "field com.datastax.oss.driver.api.testinfra.ccm.CcmBridge.DEFAULT_CLIENT_KEYSTORE_PASSWORD", + "new": "field com.datastax.oss.driver.api.testinfra.ccm.CcmBridge.DEFAULT_CLIENT_KEYSTORE_PASSWORD", + "justification": "JAVA-2620: Use clearly dummy passwords in tests" + }, + { + "code": "java.field.constantValueChanged", + "old": "field com.datastax.oss.driver.api.testinfra.ccm.CcmBridge.DEFAULT_CLIENT_TRUSTSTORE_PASSWORD", + "new": "field com.datastax.oss.driver.api.testinfra.ccm.CcmBridge.DEFAULT_CLIENT_TRUSTSTORE_PASSWORD", + "justification": "JAVA-2620: Use clearly dummy passwords in tests" + }, + { + "code": "java.field.constantValueChanged", + "old": "field com.datastax.oss.driver.api.testinfra.ccm.CcmBridge.DEFAULT_SERVER_KEYSTORE_PASSWORD", + "new": "field com.datastax.oss.driver.api.testinfra.ccm.CcmBridge.DEFAULT_SERVER_KEYSTORE_PASSWORD", + "justification": "JAVA-2620: Use clearly dummy passwords in tests" + }, + { + "code": "java.field.constantValueChanged", + "old": "field com.datastax.oss.driver.api.testinfra.ccm.CcmBridge.DEFAULT_SERVER_TRUSTSTORE_PASSWORD", + "new": "field com.datastax.oss.driver.api.testinfra.ccm.CcmBridge.DEFAULT_SERVER_TRUSTSTORE_PASSWORD", + "justification": "JAVA-2620: Use clearly dummy passwords in tests" + }, + { + "code": "java.missing.newClass", + "new": "missing-class com.datastax.oss.simulacron.common.cluster.ClusterSpec", + "justification":"Dependency was made optional" + }, + { + "code": "java.missing.newClass", + "new": "missing-class com.datastax.oss.simulacron.common.cluster.ClusterSpec.Builder", + "justification":"Dependency was made optional" + }, + { + "code": "java.missing.newClass", + "new": "missing-class com.datastax.oss.simulacron.common.cluster.QueryLog", + "justification":"Dependency was made optional" + }, + { + "code": "java.missing.newClass", + "new": "missing-class com.datastax.oss.simulacron.server.BoundCluster", + "justification":"Dependency was made optional" + }, + { + "code": "java.missing.newClass", + "new": "missing-class com.datastax.oss.simulacron.server.BoundTopic", + "justification":"Dependency was made optional" + }, + { + "code": "java.missing.newClass", + "new": "missing-class com.datastax.oss.simulacron.server.Server", + "justification":"Dependency was made optional" + }, + { + "code": "java.missing.oldClass", + "old": "missing-class com.datastax.oss.simulacron.common.cluster.ClusterSpec", + "new": "missing-class com.datastax.oss.simulacron.common.cluster.ClusterSpec", + "justification": "Dependency was made optional" + }, + { + "code": "java.missing.oldClass", + "old": "missing-class com.datastax.oss.simulacron.common.cluster.ClusterSpec.Builder", + "new": "missing-class com.datastax.oss.simulacron.common.cluster.ClusterSpec.Builder", + "justification": "Dependency was made optional" + }, + { + "code": "java.missing.oldClass", + "old": "missing-class com.datastax.oss.simulacron.common.cluster.QueryLog", + "new": "missing-class com.datastax.oss.simulacron.common.cluster.QueryLog", + "justification": "Dependency was made optional" + }, + { + "code": "java.missing.oldClass", + "old": "missing-class com.datastax.oss.simulacron.server.BoundCluster", + "new": "missing-class com.datastax.oss.simulacron.server.BoundCluster", + "justification": "Dependency was made optional" + }, + { + "code": "java.missing.oldClass", + "old": "missing-class com.datastax.oss.simulacron.server.BoundTopic", + "new": "missing-class com.datastax.oss.simulacron.server.BoundTopic", + "justification": "Dependency was made optional" + }, + { + "code": "java.missing.oldClass", + "old": "missing-class com.datastax.oss.simulacron.server.Server", + "new": "missing-class com.datastax.oss.simulacron.server.Server", + "justification": "Dependency was made optional" + }, + { + "code": "java.method.removed", + "old": "method void com.datastax.oss.driver.api.testinfra.ccm.CcmRule::reloadCore(int, java.lang.String, java.lang.String, boolean)", + "justification": "Modifying the state of a globally shared CCM instance is dangerous" + }, + { + "code": "java.method.removed", + "old": "method java.util.Optional com.datastax.oss.driver.api.testinfra.ccm.BaseCcmRule::getDseVersion()", + "justification": "Method has been replaced with more generic isDistributionOf(BackendType) and getDistributionVersion()" + }, + { + "code": "java.field.removed", + "old": "field com.datastax.oss.driver.api.testinfra.ccm.CcmBridge.DSE_ENABLEMENT", + "justification": "Method has been replaced with more generic isDistributionOf(BackendType) and getDistributionVersion()" + }, + { + "code": "java.method.nowStatic", + "old": "method com.datastax.oss.driver.api.core.Version com.datastax.oss.driver.api.testinfra.ccm.CcmBridge::getCassandraVersion()", + "new": "method com.datastax.oss.driver.api.core.Version com.datastax.oss.driver.api.testinfra.ccm.CcmBridge::getCassandraVersion()", + "justification": "Previous and current implemntation do not relay on non-static fields" + }, + { + "code": "java.method.removed", + "old": "method java.util.Optional com.datastax.oss.driver.api.testinfra.ccm.CcmBridge::getDseVersion()", + "justification": "Method has been replaced with more generic isDistributionOf(BackendType) and getDistributionVersion()" + }, + { + "ignore": true, + "code": "java.method.abstractMethodAdded", + "new": "method com.datastax.oss.driver.api.core.Version com.datastax.oss.driver.api.testinfra.CassandraResourceRule::getCassandraVersion()", + "justification": " CASSJAVA-120: ADD ASTRA INTEGRATION TESTS INFRASTRUCTURE" + }, + { + "ignore": true, + "code": "java.method.abstractMethodAdded", + "new": "method com.datastax.oss.driver.api.testinfra.requirement.BackendType com.datastax.oss.driver.api.testinfra.CassandraResourceRule::getDistribution()", + "justification": " CASSJAVA-120: ADD ASTRA INTEGRATION TESTS INFRASTRUCTURE" + }, + { + "ignore": true, + "code": "java.method.abstractMethodAdded", + "new": "method com.datastax.oss.driver.api.core.Version com.datastax.oss.driver.api.testinfra.CassandraResourceRule::getDistributionVersion()", + "justification": " CASSJAVA-120: ADD ASTRA INTEGRATION TESTS INFRASTRUCTURE" + }, + { + "ignore": true, + "code": "java.method.abstractMethodAdded", + "new": "method boolean com.datastax.oss.driver.api.testinfra.CassandraResourceRule::isDistributionOf(com.datastax.oss.driver.api.testinfra.requirement.BackendType)", + "justification": " CASSJAVA-120: ADD ASTRA INTEGRATION TESTS INFRASTRUCTURE" + }, + { + "ignore": true, + "code": "java.method.visibilityIncreased", + "old": "method void com.datastax.oss.driver.api.testinfra.ccm.BaseCcmRule::(com.datastax.oss.driver.api.testinfra.ccm.CcmBridge)", + "new": "method void com.datastax.oss.driver.api.testinfra.ccm.BaseCcmRule::(com.datastax.oss.driver.api.testinfra.ccm.CcmBridge)", + "oldVisibility": "package", + "newVisibility": "protected", + "justification": " CASSJAVA-120: ADD ASTRA INTEGRATION TESTS INFRASTRUCTURE" + }, + { + "ignore": true, + "code": "java.method.visibilityIncreased", + "old": "method void com.datastax.oss.driver.api.testinfra.ccm.CcmBridge.Builder::()", + "new": "method void com.datastax.oss.driver.api.testinfra.ccm.CcmBridge.Builder::()", + "oldVisibility": "private", + "newVisibility": "protected", + "justification": " CASSJAVA-120: ADD ASTRA INTEGRATION TESTS INFRASTRUCTURE" + }, + { + "ignore": true, + "code": "java.method.visibilityIncreased", + "old": "method void com.datastax.oss.driver.api.testinfra.ccm.CcmBridge::(java.nio.file.Path, int[], java.lang.String, java.util.Map, java.util.Map, java.util.List, java.util.List, java.util.Collection, java.util.List)", + "new": "method void com.datastax.oss.driver.api.testinfra.ccm.CcmBridge::(java.nio.file.Path, int[], java.lang.String, java.util.Map, java.util.Map, java.util.List, java.util.List, java.util.Collection, java.util.List)", + "oldVisibility": "private", + "newVisibility": "protected", + "justification": " CASSJAVA-120: ADD ASTRA INTEGRATION TESTS INFRASTRUCTURE" + }, + { + "ignore": true, + "code": "java.method.visibilityIncreased", + "old": "method void com.datastax.oss.driver.api.testinfra.ccm.CcmRule::()", + "new": "method void com.datastax.oss.driver.api.testinfra.ccm.CcmRule::()", + "oldVisibility": "private", + "newVisibility": "protected", + "justification": " CASSJAVA-120: ADD ASTRA INTEGRATION TESTS INFRASTRUCTURE" + } ] } } diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/CassandraResourceRule.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/CassandraResourceRule.java index 83c27b45e3b..4972fc958fa 100644 --- a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/CassandraResourceRule.java +++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/CassandraResourceRule.java @@ -18,7 +18,9 @@ package com.datastax.oss.driver.api.testinfra; 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.requirement.BackendType; import com.datastax.oss.driver.api.testinfra.session.SessionRule; import com.datastax.oss.driver.internal.core.metadata.DefaultEndPoint; import java.net.InetSocketAddress; @@ -58,4 +60,21 @@ public Set getContactPoints() { /** @return The highest protocol version supported by this resource. */ public abstract ProtocolVersion getHighestProtocolVersion(); + + /** @return The backend distribution type (CASSANDRA, DSE, HCD, ASTRA). */ + public abstract BackendType getDistribution(); + + /** + * Checks if this resource is of the specified backend type. + * + * @param type the backend type to check + * @return true if this resource is of the specified type + */ + public abstract boolean isDistributionOf(BackendType type); + + /** @return The distribution version (e.g., DSE version, Cassandra version). */ + public abstract Version getDistributionVersion(); + + /** @return The Cassandra version (may differ from distribution version for DSE/HCD). */ + public abstract Version getCassandraVersion(); } diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/astra/AstraBridge.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/astra/AstraBridge.java new file mode 100644 index 00000000000..c85b9288fff --- /dev/null +++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/astra/AstraBridge.java @@ -0,0 +1,639 @@ +/* + * 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.CqlIdentifier; +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.Version; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata; +import com.datastax.oss.driver.api.core.metadata.schema.TableMetadata; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.api.testinfra.ccm.CcmBridge; +import com.datastax.oss.driver.api.testinfra.requirement.BackendType; +import com.datastax.oss.driver.shaded.guava.common.base.Splitter; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.commons.exec.CommandLine; +import org.apache.commons.exec.DefaultExecutor; +import org.apache.commons.exec.ExecuteStreamHandler; +import org.apache.commons.exec.ExecuteWatchdog; +import org.apache.commons.exec.Executor; +import org.apache.commons.exec.LogOutputStream; +import org.apache.commons.exec.PumpStreamHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AstraBridge extends CcmBridge { + + private static final Logger LOG = LoggerFactory.getLogger(AstraBridge.class); + + public static final BackendType DISTRIBUTION = BackendType.ASTRA; + + // Astra CLI configuration + private static final String ASTRA_TOKEN = System.getProperty("astra.token"); + private static final String ASTRA_CLOUD_PROVIDER = + System.getProperty("astra.cloud.provider", "gcp"); + private static final String ASTRA_REGION = System.getProperty("astra.region", "us-east1"); + + // Existing database ID (if provided, use existing database instead of creating new one) + private static final String ASTRA_DATABASE_ID = System.getProperty("astra.database.id"); + + // Database configuration + private static final String DATABASE_NAME_PREFIX = "java_driver_test_db_"; + private static final String DEFAULT_KEYSPACE = "java_driver_test"; + + private final String databaseName; + private final String keyspace; + + @SuppressWarnings("UnusedVariable") + private final String cloudProvider; + + private final String region; + private final AtomicBoolean created = new AtomicBoolean(); + private final AtomicBoolean started = new AtomicBoolean(); + private final Path configDirectory; + + // Flag to track if we're using an existing database (should not be destroyed) + private final boolean usingExistingDatabase; + + private String databaseId; + private File secureConnectBundle; + + private AstraBridge( + Path configDirectory, + String databaseName, + String keyspace, + String cloudProvider, + String region, + String existingDatabaseId) { + super( + configDirectory, + new int[] {1}, + "127.0.0", + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList()); + this.configDirectory = configDirectory; + this.keyspace = keyspace; + this.cloudProvider = cloudProvider; + this.region = region; + this.usingExistingDatabase = existingDatabaseId != null; + this.databaseId = existingDatabaseId; // Will be set if using existing database + + // If using existing database, extract the database name from Astra + if (usingExistingDatabase) { + try { + // Setup Astra CLI with token first + runAstraCommand( + "config", + "create", + "java_driver_test", + "--token", + ASTRA_TOKEN, + "--default", + "--overwrite"); + + // Get database info using CSV output format + String dbInfoOutput = runAstraCommand("db", "get", existingDatabaseId, "-o", "csv"); + this.databaseName = extractDatabaseNameFromCsv(dbInfoOutput); + LOG.info( + "Extracted database name '{}' for existing database ID: {}", + this.databaseName, + existingDatabaseId); + } catch (IOException | InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException( + "Failed to extract database name for existing database ID: " + existingDatabaseId, e); + } + } else { + this.databaseName = databaseName; + } + } + + public static Builder builder() { + return new Builder(); + } + + public static boolean isDistributionOf(BackendType type) { + return DISTRIBUTION == type; + } + + public static Version getDistributionVersion() { + // Astra uses Cassandra 4.0 + return Version.parse("4.0.0"); + } + + public static Version getCassandraVersion() { + // Astra uses Cassandra 4.0 + return Version.parse("4.0.0"); + } + + public static class Builder extends CcmBridge.Builder { + private String databaseName = DATABASE_NAME_PREFIX + System.currentTimeMillis(); + private String keyspace = DEFAULT_KEYSPACE; + private String cloudProvider = ASTRA_CLOUD_PROVIDER; + private String region = ASTRA_REGION; + private String existingDatabaseId = ASTRA_DATABASE_ID; + + public Builder withDatabaseName(String databaseName) { + this.databaseName = databaseName; + return this; + } + + public Builder withKeyspace(String keyspace) { + this.keyspace = keyspace; + return this; + } + + public Builder withCloudProvider(String cloudProvider) { + this.cloudProvider = cloudProvider; + return this; + } + + public Builder withRegion(String region) { + this.region = region; + return this; + } + + public Builder withExistingDatabaseId(String databaseId) { + this.existingDatabaseId = databaseId; + return this; + } + + @Override + public AstraBridge build() { + try { + Path configDir = Files.createTempDirectory("astra-test-"); + return new AstraBridge( + configDir, databaseName, keyspace, cloudProvider, region, existingDatabaseId); + } catch (IOException e) { + throw new RuntimeException("Failed to create config directory", e); + } + } + } + + @Override + public synchronized void create() { + if (created.compareAndSet(false, true)) { + try { + // Setup Astra CLI with token (skip if already done in constructor for existing DB) + if (!usingExistingDatabase) { + runAstraCommand( + "config", + "create", + "java_driver_test", + "--token", + ASTRA_TOKEN, + "--default", + "--overwrite"); + } + + if (usingExistingDatabase) { + LOG.info("Using existing Astra database '{}' with ID: {}", databaseName, databaseId); + + // Download secure connect bundle for existing database + downloadSecureConnectBundleById(); + + } else { + LOG.info("Creating Astra database: {}", databaseName); + + // Create database using Astra CLI + // astra db create --no-async --non-vector --if-not-exists -k -r + // + List createArgs = new ArrayList<>(); + createArgs.add("db"); + createArgs.add("create"); + createArgs.add("--no-async"); + createArgs.add("--non-vector"); + createArgs.add("--if-not-exists"); + createArgs.add("-k"); + createArgs.add(keyspace); + createArgs.add("-r"); + createArgs.add(region); + createArgs.add(databaseName); + + String output = runAstraCommand(createArgs.toArray(new String[0])); + LOG.info("Database creation output: {}", output); + + // Get database ID using: astra db get --key id + String dbIdOutput = runAstraCommand("db", "get", databaseName, "--key", "id"); + LOG.info("Database ID output: {}", dbIdOutput); + + // Extract the UUID from the output (filter out [INFO] and other lines) + databaseId = extractDatabaseId(dbIdOutput); + LOG.info("Astra database created with ID: {}", databaseId); + + // Download secure connect bundle + downloadSecureConnectBundle(); + } + + } catch (IOException | InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Failed to create Astra database", e); + } + } + } + + private void downloadSecureConnectBundle() throws IOException, InterruptedException { + // Create SCB directory + Path scbDir = configDirectory.resolve("scb"); + Files.createDirectories(scbDir); + + // Download SCB using: astra db download-scb -f + File scbFile = scbDir.resolve("scb_" + databaseId + "_" + region + ".zip").toFile(); + runAstraCommand("db", "download-scb", databaseName, "-f", scbFile.getAbsolutePath()); + + if (!scbFile.exists()) { + throw new IOException("Secure connect bundle was not downloaded: " + scbFile); + } + + this.secureConnectBundle = scbFile; + LOG.info("Secure connect bundle downloaded to: {}", scbFile.getAbsolutePath()); + } + + private void downloadSecureConnectBundleById() throws IOException, InterruptedException { + // Create SCB directory + Path scbDir = configDirectory.resolve("scb"); + Files.createDirectories(scbDir); + + // Download SCB using database ID: astra db download-scb -f + File scbFile = scbDir.resolve("scb_" + databaseId + ".zip").toFile(); + runAstraCommand("db", "download-scb", databaseId, "-f", scbFile.getAbsolutePath()); + + if (!scbFile.exists()) { + throw new IOException("Secure connect bundle was not downloaded: " + scbFile); + } + + this.secureConnectBundle = scbFile; + LOG.info("Secure connect bundle downloaded by ID to: {}", scbFile.getAbsolutePath()); + } + + private static final Pattern UUID_PATTERN = + Pattern.compile( + "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"); + + /** + * Extract database ID (UUID) from Astra CLI output. The output may contain [INFO] lines and other + * messages, so we need to find the line that looks like a UUID. E.g. ✗ astra db get + * java_driver_test_db_1765404086049 --key id [INFO] You are using a non-production environment + * 'DEV' be2d3ad0-3bb0-4257-97d6-b83f2265b5f1 + */ + private String extractDatabaseId(String output) { + // Use Pattern.compile to split by newline to avoid String.split() warning + String[] lines = Pattern.compile("\n").split(output); + for (String line : lines) { + String trimmed = line.trim(); + // Skip lines that start with [INFO], [OK], [ERROR], etc. + if (trimmed.startsWith("[")) { + continue; + } + // Check if this line contains a UUID + Matcher matcher = UUID_PATTERN.matcher(trimmed); + if (matcher.find()) { + return matcher.group(); + } + } + throw new IllegalStateException("Could not extract database ID from output: " + output); + } + + /** + * Extract database name from Astra CLI CSV output. The CSV output format is: + * + *
+   * Attribute,Value
+   * Name,java_driver_test_db_1767831564069
+   * id,67750433-fe2e-48d4-bd60-7aef5e76be27
+   * ...
+   * 
+ * + * @param output the CSV output from 'astra db get -o csv' + * @return the database name + */ + private String extractDatabaseNameFromCsv(String output) { + // Use Pattern.compile to split by newline to avoid String.split() warning + String[] lines = Pattern.compile("\n").split(output); + for (String line : lines) { + String trimmed = line.trim(); + // Skip lines that start with [INFO], [OK], [ERROR], etc. + if (trimmed.startsWith("[")) { + continue; + } + // Skip the CSV header line + if (trimmed.startsWith("code,message,attribute,value")) { + continue; + } + // Look for the line with "Name" in the attribute column + // Format: OK,,Name, or Name, (for older CLI versions) + if (trimmed.contains(",Name,")) { + // Split by comma and get the last part (the value) + List parts = Splitter.on(',').splitToList(trimmed); + if (parts.size() >= 2) { + // Return the last part which is the database name + return parts.get(parts.size() - 1).trim(); + } + } else if (trimmed.startsWith("Name,")) { + // Handle older CSV format: Name, + 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 baseBuilder( + CassandraResourceRule cassandraResource, CqlIdentifier keyspace) { + return builder(cassandraResource, keyspace, null, null, null); + } + private static SessionBuilder builder( CassandraResourceRule cassandraResource, CqlIdentifier keyspace, @@ -147,8 +164,31 @@ private static SessionBuilder builder( SchemaChangeListener schemaChangeListener, Predicate nodeFilter) { SessionBuilder 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"); + } }