Skip to content
359 changes: 274 additions & 85 deletions driver-core/src/test/java/com/datastax/driver/core/CCMBridge.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
package com.datastax.driver.core;

import static org.assertj.core.api.Assertions.assertThat;

import com.datastax.driver.core.CCMBridge.Builder.ResolvedVersions;
import com.google.common.collect.ImmutableMap;
import java.util.Map;
import org.testng.annotations.Test;

/**
* Unit tests for the part of {@link CCMBridge.Builder} that decides which server flavor and version
* to install, i.e. the {@code ccm create} command, the environment it runs in and the
* flavor-specific yaml it writes. No CCM cluster is created.
*
* <p>Each test configures the flavor explicitly, so that it doesn't depend on the {@code
* scylla.version} / {@code dse} system properties of the surrounding run.
*/
public class CCMBridgeCreateCommandTest {

@Test(groups = "unit")
public void should_create_scylla_cluster_when_scylla_version_configured() {
CCMBridge.Builder builder =
CCMBridge.builder()
.withDSE(false)
.withScylla(true)
.withVersion(VersionNumber.parse("2026.1.0"));

ResolvedVersions versions = builder.resolveVersions();
assertThat(versions.scylla).isEqualTo(VersionNumber.parse("2026.1.0"));
assertThat(versions.cassandra).isEqualTo(VersionNumber.parse("3.0.8"));
assertThat(versions.dse).isNull();

String command = builder.buildCreateCommand("test_cluster", versions);
assertThat(command).contains("--scylla").contains("-v release:2026.1.0");
assertThat(command).doesNotContain("--dse");
// 3.0.8 is only what Scylla reports in system.local, it is never an install target
assertThat(command).doesNotContain("3.0.8");

// 2026.1.0 is an Enterprise version, it must not be installed from the OSS repository
assertThat(CCMBridge.Builder.buildEnvironmentMap(versions))
.containsEntry("SCYLLA_PRODUCT", "enterprise");
}

@Test(groups = "unit")
public void should_not_use_enterprise_repository_for_open_source_scylla_version() {
CCMBridge.Builder builder =
CCMBridge.builder()
.withDSE(false)
.withScylla(true)
.withVersion(VersionNumber.parse("6.2.0"));

ResolvedVersions versions = builder.resolveVersions();
assertThat(versions.scylla).isEqualTo(VersionNumber.parse("6.2.0"));

// Would leak in from a `-Dscylla.version=<year>.<x>` run if the product was global
assertThat(CCMBridge.Builder.buildEnvironmentMap(versions)).doesNotContainKey("SCYLLA_PRODUCT");
}

@Test(groups = "unit")
public void should_create_cassandra_cluster_when_cassandra_version_configured() {
CCMBridge.Builder builder =
CCMBridge.builder()
.withDSE(false)
.withScylla(false)
.withVersion(VersionNumber.parse("4.1.3"));

ResolvedVersions versions = builder.resolveVersions();
assertThat(versions.cassandra).isEqualTo(VersionNumber.parse("4.1.3"));
assertThat(versions.dse).isNull();
assertThat(versions.scylla).isNull();

String command = builder.buildCreateCommand("test_cluster", versions);
assertThat(command).contains("-v 4.1.3");
assertThat(command).doesNotContain("--scylla").doesNotContain("--dse");

assertThat(CCMBridge.Builder.buildEnvironmentMap(versions)).doesNotContainKey("SCYLLA_PRODUCT");
}

/**
* The globally configured version keeps the environment built for it in the static initializer:
* that one is derived from the raw {@code scylla.version} string, which may be a branch spec
* whose resolved version number looks like an Enterprise one without being installed as such.
*/
@Test(groups = "unit")
public void should_use_global_environment_when_no_version_configured() {
VersionNumber cassandra = VersionNumber.parse("3.0.8");
VersionNumber enterpriseScylla = VersionNumber.parse("2026.1.0");

assertThat(
CCMBridge.Builder.buildEnvironmentMap(
new ResolvedVersions(false, cassandra, null, enterpriseScylla)))
.isSameAs(
CCMBridge.Builder.buildEnvironmentMap(
new ResolvedVersions(false, cassandra, null, null)));
}

/**
* Scylla reads a PEM certificate and key, not the JKS keystore Cassandra reads, so an explicitly
* configured Scylla cluster must not be given the Cassandra settings just because the surrounding
* run has no {@code scylla.version}.
*/
@Test(groups = "unit")
public void should_use_pem_client_encryption_for_configured_scylla_version() {
CCMBridge.Builder builder =
CCMBridge.builder()
.withDSE(false)
.withScylla(true)
.withVersion(VersionNumber.parse("2026.1.0"))
.withAuth();

Map<String, Object> options = builder.buildClientEncryptionOptions(builder.resolveVersions());

assertThat(options)
.containsEntry("client_encryption_options.enabled", "true")
.containsEntry("client_encryption_options.require_client_auth", "true")
.containsKey("client_encryption_options.certificate")
.containsKey("client_encryption_options.keyfile")
.containsKey("client_encryption_options.truststore");
assertThat(options)
.doesNotContainKey("client_encryption_options.keystore")
.doesNotContainKey("client_encryption_options.keystore_password")
.doesNotContainKey("client_encryption_options.truststore_password");
}

/** The mirror image: an explicit Cassandra version under a global Scylla run. */
@Test(groups = "unit")
public void should_use_keystore_client_encryption_for_configured_cassandra_version() {
CCMBridge.Builder builder =
CCMBridge.builder()
.withDSE(false)
.withScylla(false)
.withVersion(VersionNumber.parse("4.1.3"))
.withAuth();

Map<String, Object> options = builder.buildClientEncryptionOptions(builder.resolveVersions());

assertThat(options)
.containsEntry("client_encryption_options.enabled", "true")
.containsEntry("client_encryption_options.require_client_auth", "true")
.containsKey("client_encryption_options.keystore")
.containsKey("client_encryption_options.keystore_password")
.containsKey("client_encryption_options.truststore")
.containsKey("client_encryption_options.truststore_password");
assertThat(options)
.doesNotContainKey("client_encryption_options.certificate")
.doesNotContainKey("client_encryption_options.keyfile");
}

/** {@code withSSL()} alone must not enable client certificate authentication. */
@Test(groups = "unit")
public void should_not_require_client_auth_without_with_auth() {
CCMBridge.Builder sslOnly = CCMBridge.builder().withDSE(false).withScylla(true).withSSL();
assertThat(sslOnly.buildClientEncryptionOptions(sslOnly.resolveVersions()))
.containsEntry("client_encryption_options.enabled", "true")
.doesNotContainKey("client_encryption_options.require_client_auth");

CCMBridge.Builder plaintext = CCMBridge.builder().withDSE(false).withScylla(true);
assertThat(plaintext.buildClientEncryptionOptions(plaintext.resolveVersions())).isEmpty();
}

/**
* {@code ssl}/{@code auth} are no longer reflected in {@code cassandraConfiguration} at
* configuration time, so {@link CCMBridge.Builder} has to compare them itself: {@link CCMCache}
* keys cached clusters on the builder, and would otherwise hand an encrypted cluster to a test
* that asked for a plaintext one.
*/
@Test(groups = "unit")
public void should_not_consider_encrypted_and_plaintext_clusters_equal() {
CCMBridge.Builder plaintext = CCMBridge.builder().withNodes(1);
CCMBridge.Builder encrypted = CCMBridge.builder().withNodes(1).withSSL();
CCMBridge.Builder authenticated = CCMBridge.builder().withNodes(1).withAuth();

assertThat(plaintext).isNotEqualTo(encrypted).isNotEqualTo(authenticated);
assertThat(encrypted).isNotEqualTo(authenticated);
assertThat(encrypted).isEqualTo(CCMBridge.builder().withNodes(1).withSSL());
assertThat(encrypted.hashCode())
.isEqualTo(CCMBridge.builder().withNodes(1).withSSL().hashCode());
}

@Test(groups = "unit")
public void should_create_dse_cluster_when_dse_version_configured() {
CCMBridge.Builder builder =
CCMBridge.builder()
.withDSE(true)
.withScylla(false)
.withVersion(VersionNumber.parse("6.8.0"));

ResolvedVersions versions = builder.resolveVersions();
assertThat(versions.dse).isEqualTo(VersionNumber.parse("6.8.0"));
assertThat(versions.cassandra).isNotNull();
assertThat(versions.scylla).isNull();

String command = builder.buildCreateCommand("test_cluster", versions);
assertThat(command).contains("--dse").contains("-v 6.8.0");
assertThat(command).doesNotContain("--scylla");
}

/**
* An environment as inherited from a shell that exported {@code SCYLLA_PRODUCT}, e.g. left over
* from an earlier step of the same CI job.
*/
private static Map<String, String> inheritedEnterpriseEnvironment() {
return ImmutableMap.of("PATH", "/usr/bin", "SCYLLA_PRODUCT", "enterprise");
}

@Test(groups = "unit")
public void should_use_enterprise_repository_for_global_enterprise_version() {
Map<String, String> environment =
CCMBridge.buildGlobalEnvironmentMap(inheritedEnterpriseEnvironment(), true, false);

assertThat(environment).containsEntry("SCYLLA_PRODUCT", "enterprise");
assertThat(environment).containsEntry("PATH", "/usr/bin");
}

/**
* The global version is a number that isn't Enterprise, so the repository is known: an inherited
* value must not override it, or an OSS version is looked up in the Enterprise repository.
*/
@Test(groups = "unit")
public void should_drop_inherited_product_for_global_open_source_version() {
Map<String, String> environment =
CCMBridge.buildGlobalEnvironmentMap(inheritedEnterpriseEnvironment(), false, false);

assertThat(environment).doesNotContainKey("SCYLLA_PRODUCT");
assertThat(environment).containsEntry("PATH", "/usr/bin");
}

/**
* A branch spec can't be classified as Enterprise or OSS by its version string, so exporting
* {@code SCYLLA_PRODUCT} is the only way to select the repository: that one inherited value has
* to survive.
*/
@Test(groups = "unit")
public void should_keep_inherited_product_for_global_branch_spec() {
Map<String, String> environment =
CCMBridge.buildGlobalEnvironmentMap(inheritedEnterpriseEnvironment(), false, true);

assertThat(environment).containsEntry("SCYLLA_PRODUCT", "enterprise");
}

/**
* A pure Cassandra run, or no configured version at all: nothing about the run asks for the
* Enterprise repository, so a stale inherited value must not reach ccm.
*/
@Test(groups = "unit")
public void should_drop_inherited_product_when_no_scylla_version_configured() {
Map<String, String> environment =
CCMBridge.buildGlobalEnvironmentMap(
ImmutableMap.of("JAVA_HOME", "/opt/java", "SCYLLA_PRODUCT", "enterprise"),
false,
false);

assertThat(environment).doesNotContainKey("SCYLLA_PRODUCT");
assertThat(environment).containsEntry("JAVA_HOME", "/opt/java");
}
}
25 changes: 22 additions & 3 deletions driver-core/src/test/java/com/datastax/driver/core/CCMConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,20 @@ final class Undefined {}
int[] numberOfNodes() default {};

/**
* The C* or DSE version to use; defaults to the version defined by the System property {@code
* cassandra.version}.
* The C*, DSE or Scylla version to use; defaults to the version defined by the System property
* {@code cassandra.version}.
*
* <p>Note that setting this attribute completely overrides the System properties {@code
* cassandra.version} and {@code cassandra.directory}.
*
* <p>Which server this version names is decided by {@link #dse()} and {@link #scylla()}, which
* default to the flavor of the surrounding run. Set the matching one explicitly whenever this
* attribute is set, or a Cassandra version will be installed as Scylla (or vice versa) depending
* on how the test run was invoked.
*
* <p>This attribute is ignored if {@link #ccmProvider()} is defined.
*
* @return The C* or DSE version to use
* @return The C*, DSE or Scylla version to use
* @see CCMBridge#getCassandraVersion()
*/
String version() default "";
Expand All @@ -75,6 +80,20 @@ final class Undefined {}
*/
boolean[] dse() default {};

/**
* Whether to launch a Scylla instance rather than an OSS C*.
*
* <p>Note that setting this attribute completely overrides the System property {@code
* scylla.version}: only whether Scylla is launched, not which version. Set it together with
* {@link #version()} so that an explicitly configured version is installed as the server it
* actually names, instead of inheriting the flavor of the surrounding run.
*
* <p>This attribute is ignored if {@link #ccmProvider()} is defined.
*
* @return {@code true} to launch a Scylla instance, {@code false} to launch an OSS C* instance.
*/
boolean[] scylla() default {};

/**
* Configuration items to add to cassandra.yaml configuration file. Each configuration item must
* be in the form {@code key:value}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,14 @@ private Boolean dse() {
return null;
}

@SuppressWarnings("SimplifiableIfStatement")
private Boolean scylla() {
for (CCMConfig ann : annotations) {
if (ann != null && ann.scylla().length > 0) return ann.scylla()[0];
}
return null;
}

@SuppressWarnings("SimplifiableIfStatement")
private boolean ssl() {
for (CCMConfig ann : annotations) {
Expand Down Expand Up @@ -497,14 +505,19 @@ private CCMBridge.Builder ccmBuilder(Object testInstance) throws Exception {
ccmBuilder = CCMBridge.builder().withNodes(numberOfNodes()).notStarted();
}

// Set the flavor before the version: which server an explicitly configured version names
// is decided by these flags, which otherwise default to the flavor of the surrounding run.
Boolean dse = dse();
if (dse != null) ccmBuilder.withDSE(dse);
Boolean scylla = scylla();
if (scylla != null) ccmBuilder.withScylla(scylla);

String versionStr = version();
if (versionStr != null) {
VersionNumber version = VersionNumber.parse(versionStr);
ccmBuilder.withVersion(version);
}

Boolean dse = dse();
if (dse != null) ccmBuilder.withDSE(dse);
if (ssl()) ccmBuilder.withSSL();
if (auth()) ccmBuilder.withAuth();
for (Map.Entry<String, Object> entry : config().entrySet()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public void should_fail_when_beta_allowed_and_too_high() {

/** @jira_ticket JAVA-1367 */
@Test(groups = "short", enabled = false /* @IntegrationTestDisabledCassandra3Failure */)
@CCMConfig(version = "2.1.16", createCluster = false)
@CCMConfig(version = "2.1.16", scylla = false, createCluster = false)
public void should_negotiate_when_no_version_provided() {
if (protocolVersion.compareTo(ProtocolVersion.NEWEST_SUPPORTED) >= 0) {
throw new SkipException("Server supports newest protocol version driver supports");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ public void should_ignore_node_that_does_not_support_protocol_version_on_session
.withStoragePort(mainCcm.getStoragePort())
.withThriftPort(mainCcm.getThriftPort())
.withBinaryPort(mainCcm.getBinaryPort())
// 2.1.20 is a Cassandra version: say so, or a Scylla run would install it as Scylla.
.withScylla(false)
.withVersion(VersionNumber.parse("2.1.20"));
otherCcm = CCMCache.get(otherCcmBuilder);
otherCcm.waitForUp(1);
Expand Down
Loading
Loading