diff --git a/client-java/instrumentation-shared/src/main/java/org/evomaster/client/java/instrumentation/shared/ReplacementCategory.java b/client-java/instrumentation-shared/src/main/java/org/evomaster/client/java/instrumentation/shared/ReplacementCategory.java
index 92d00f8f4e..e7a2468d95 100644
--- a/client-java/instrumentation-shared/src/main/java/org/evomaster/client/java/instrumentation/shared/ReplacementCategory.java
+++ b/client-java/instrumentation-shared/src/main/java/org/evomaster/client/java/instrumentation/shared/ReplacementCategory.java
@@ -37,6 +37,11 @@ public enum ReplacementCategory {
*/
MONGO,
+ /**
+ * Replacements to handle CASSANDRA command intereception
+ */
+ CASSANDRA,
+
/**
* Replacements to handle OPENSEARCH command interceptions
*/
diff --git a/client-java/instrumentation/pom.xml b/client-java/instrumentation/pom.xml
index fe05f25f9e..5f4fe6e999 100644
--- a/client-java/instrumentation/pom.xml
+++ b/client-java/instrumentation/pom.xml
@@ -205,6 +205,11 @@
mongodb-driver-sync
test
+
+ org.apache.cassandra
+ java-driver-core
+ test
+
org.neo4j.driver
neo4j-java-driver
diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/AdditionalInfo.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/AdditionalInfo.java
index 71bb8c78f5..9493cfb078 100644
--- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/AdditionalInfo.java
+++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/AdditionalInfo.java
@@ -106,6 +106,8 @@ public StatementDescription(String line, String method) {
private final Set mongoFindCommandData = new CopyOnWriteArraySet<>();
+ private final Set executedCqlCommandData = new CopyOnWriteArraySet<>();
+
private final Set neo4JRunCommandData = new CopyOnWriteArraySet<>();
private final Set openSearchCommandData = new CopyOnWriteArraySet<>();
@@ -124,6 +126,10 @@ public Set getMongoInfoData(){
return Collections.unmodifiableSet(mongoFindCommandData);
}
+ public Set getCqlInfoData(){
+ return Collections.unmodifiableSet(executedCqlCommandData);
+ }
+
public Set getNeo4JInfoData(){
return Collections.unmodifiableSet(neo4JRunCommandData);
}
@@ -152,6 +158,10 @@ public void addMongoInfo(MongoFindCommand info){
mongoFindCommandData.add(info);
}
+ public void addCqlInfo(ExecutedCqlCommand info){
+ executedCqlCommandData.add(info);
+ }
+
public void addNeo4JInfo(Neo4JRunCommand info){
neo4JRunCommandData.add(info);
}
diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/ExecutedCqlCommand.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/ExecutedCqlCommand.java
new file mode 100644
index 0000000000..e4bbdd5901
--- /dev/null
+++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/ExecutedCqlCommand.java
@@ -0,0 +1,61 @@
+package org.evomaster.client.java.instrumentation;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * Info related to CQL command execution
+ */
+public class ExecutedCqlCommand implements Serializable {
+
+ /**
+ * A constant to represent that execution time could not be obtained.
+ */
+ public static final long FAILURE_EXECUTION_TIME = -1L;
+
+ /**
+ * The actual CQL string with the command that was executed
+ */
+ private final String cqlCommand;
+
+ /**
+ * Whether the CQL command failed, for any reason
+ */
+ private final boolean threwCqlException;
+
+ /**
+ * Execution time
+ */
+ private final long executionTime;
+
+ public ExecutedCqlCommand(String cqlCommand, boolean threwCqlException, long executionTime) {
+ this.cqlCommand = cqlCommand;
+ this.threwCqlException = threwCqlException;
+ this.executionTime = executionTime;
+ }
+
+ public String getCqlCommand() {
+ return cqlCommand;
+ }
+
+ public boolean hasThrownCqlException() {
+ return threwCqlException;
+ }
+
+ public long getExecutionTime() {
+ return executionTime;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ExecutedCqlCommand executedCqlCommand = (ExecutedCqlCommand) o;
+ return threwCqlException == executedCqlCommand.threwCqlException && Objects.equals(cqlCommand, executedCqlCommand.cqlCommand);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(cqlCommand, threwCqlException);
+ }
+}
diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/InstrumentationController.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/InstrumentationController.java
index 36db7558db..52c3e34cda 100644
--- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/InstrumentationController.java
+++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/InstrumentationController.java
@@ -51,6 +51,10 @@ public static void setExecutingInitMongo(boolean executingInitMongo){
ExecutionTracer.setExecutingInitMongo(executingInitMongo);
}
+ public static void setExecutingInitCassandra(boolean executingInitCassandra){
+ ExecutionTracer.setExecutingInitCassandra(executingInitCassandra);
+ }
+
public static void setExecutingInitRedis(boolean executingInitRedis){
ExecutionTracer.setExecutingInitRedis(executingInitRedis);
}
diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/ReplacementList.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/ReplacementList.java
index 71daeddaad..24f2b9d3e2 100644
--- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/ReplacementList.java
+++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/ReplacementList.java
@@ -31,6 +31,7 @@ public static List getList() {
new ByteClassReplacement(),
new CharacterClassReplacement(),
new CollectionClassReplacement(),
+ new CqlSessionClassReplacement(),
new CursorPreparerClassReplacement(),
new DateClassReplacement(),
new DateFormatClassReplacement(),
diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacement.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacement.java
new file mode 100644
index 0000000000..641a68ebd0
--- /dev/null
+++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacement.java
@@ -0,0 +1,50 @@
+package org.evomaster.client.java.instrumentation.coverage.methodreplacement.thirdpartyclasses;
+
+import org.evomaster.client.java.instrumentation.ExecutedCqlCommand;
+import org.evomaster.client.java.instrumentation.coverage.methodreplacement.Replacement;
+import org.evomaster.client.java.instrumentation.coverage.methodreplacement.ThirdPartyMethodReplacementClass;
+import org.evomaster.client.java.instrumentation.coverage.methodreplacement.UsageFilter;
+import org.evomaster.client.java.instrumentation.shared.ReplacementCategory;
+import org.evomaster.client.java.instrumentation.shared.ReplacementType;
+import org.evomaster.client.java.instrumentation.staticstate.ExecutionTracer;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public class CqlSessionClassReplacement extends ThirdPartyMethodReplacementClass {
+ private static final CqlSessionClassReplacement singleton = new CqlSessionClassReplacement();
+
+ public static final String CASSANDRA_FIND_STRING_SYNC = "cassandraExecuteStringSync";
+
+ @Override
+ protected String getNameOfThirdPartyTargetClass() {
+ return "com.datastax.oss.driver.api.core.CqlSession";
+ }
+
+ @Replacement(type = ReplacementType.TRACKER, id = CASSANDRA_FIND_STRING_SYNC, usageFilter = UsageFilter.ANY, category = ReplacementCategory.CASSANDRA, castTo = "com.datastax.oss.driver.api.core.cql.ResultSet")
+ public static Object execute(Object cqlSession, String query) {
+ return handleCqlExecute(CASSANDRA_FIND_STRING_SYNC, cqlSession, query);
+ }
+
+ private static Object handleCqlExecute(String id, Object cqlSession, String query) {
+ long start = System.currentTimeMillis();
+ try {
+ Method executeMethod = retrieveExecuteMethod(id, cqlSession);
+ Object result = executeMethod.invoke(cqlSession, query);
+ long end = System.currentTimeMillis();
+ long executionTime = end - start;
+ ExecutedCqlCommand info = new ExecutedCqlCommand(query, false, executionTime);
+ ExecutionTracer.addCqlInfo(info);
+ return result;
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ } catch (InvocationTargetException e) {
+ throw (RuntimeException) e.getCause();
+ }
+ }
+
+ private static Method retrieveExecuteMethod(String id, Object cqlSession){
+ return getOriginal(singleton, id, cqlSession);
+ }
+
+}
\ No newline at end of file
diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/AgentController.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/AgentController.java
index f474924be7..79aa3b7ff2 100644
--- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/AgentController.java
+++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/AgentController.java
@@ -14,7 +14,7 @@
/**
* Code running in the Java Agent to receive and respond to the
- * requests from the the SUT controller.
+ * requests from the SUT controller.
*/
public class AgentController {
@@ -94,6 +94,10 @@ public static void start(int port){
handleExecutingInitMongo();
sendCommand(Command.ACK);
break;
+ case EXECUTING_INIT_CASSANDRA:
+ handleExecutingInitCassandra();
+ sendCommand(Command.ACK);
+ break;
case EXECUTING_INIT_REDIS:
handleExecutingInitRedis();
sendCommand(Command.ACK);
@@ -184,6 +188,16 @@ private static void handleExecutingInitMongo() {
}
}
+ private static void handleExecutingInitCassandra() {
+ try {
+ Object msg = in.readObject();
+ Boolean executingInitCassandra = (Boolean) msg;
+ InstrumentationController.setExecutingInitCassandra(executingInitCassandra);
+ } catch (Exception e){
+ SimpleLogger.error("Failure in handling executing-init-cassandra: "+e.getMessage());
+ }
+ }
+
private static void handleExecutingInitRedis() {
try {
Object msg = in.readObject();
diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/Command.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/Command.java
index 2c02a3aa7f..f7a7ba2ac6 100644
--- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/Command.java
+++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/external/Command.java
@@ -19,6 +19,7 @@ public enum Command implements Serializable {
KILL_SWITCH,
EXECUTING_INIT_SQL,
EXECUTING_INIT_MONGO,
+ EXECUTING_INIT_CASSANDRA,
EXECUTING_INIT_REDIS,
EXECUTING_ACTION,
BOOT_TIME_INFO,
diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/staticstate/ExecutionTracer.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/staticstate/ExecutionTracer.java
index a386620c49..2386227c12 100644
--- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/staticstate/ExecutionTracer.java
+++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/staticstate/ExecutionTracer.java
@@ -35,6 +35,8 @@ public class ExecutionTracer {
private static boolean executingInitMongo = false;
+ private static boolean executingInitCassandra = false;
+
private static boolean executingInitRedis = false;
private static boolean executingInitNeo4J = false;
@@ -202,6 +204,10 @@ public static void setExecutingInitMongo(boolean executingInitMongo) {
ExecutionTracer.executingInitMongo = executingInitMongo;
}
+ public static void setExecutingInitCassandra(boolean executingInitCassandra) {
+ ExecutionTracer.executingInitCassandra = executingInitCassandra;
+ }
+
public static void setExecutingInitRedis(boolean executingInitRedis) {
ExecutionTracer.executingInitRedis = executingInitRedis;
}
@@ -442,6 +448,11 @@ public static void addMongoInfo(MongoFindCommand info){
getCurrentAdditionalInfo().addMongoInfo(info);
}
+ public static void addCqlInfo(ExecutedCqlCommand info){
+ if (!executingInitCassandra)
+ getCurrentAdditionalInfo().addCqlInfo(info);
+ }
+
public static void addNeo4JInfo(Neo4JRunCommand info){
if (!executingInitNeo4J)
getCurrentAdditionalInfo().addNeo4JInfo(info);
diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacementTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacementTest.java
new file mode 100644
index 0000000000..38a80e78d4
--- /dev/null
+++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacementTest.java
@@ -0,0 +1,143 @@
+package org.evomaster.client.java.instrumentation.coverage.methodreplacement.thirdpartyclasses;
+
+import com.datastax.oss.driver.api.core.CqlSession;
+import org.evomaster.client.java.instrumentation.AdditionalInfo;
+import org.evomaster.client.java.instrumentation.ExecutedCqlCommand;
+import org.evomaster.client.java.instrumentation.staticstate.ExecutionTracer;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+
+import java.net.InetSocketAddress;
+import java.time.Duration;
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class CqlSessionClassReplacementTest {
+
+ private static CqlSession cqlSession;
+ private static final int CASSANDRA_PORT = 9042;
+ private static final String CASSANDRA_IMAGE = "cassandra";
+ private static final String CASSANDRA_VERSION = "4.1";
+
+ private static final GenericContainer> cassandra = new GenericContainer<>(CASSANDRA_IMAGE + ":" + CASSANDRA_VERSION)
+ .withExposedPorts(CASSANDRA_PORT)
+ .waitingFor(Wait.forLogMessage(".*Starting listening for CQL clients.*", 1)
+ .withStartupTimeout(Duration.ofMinutes(2)));
+
+ private static final String KEYSPACE = "testks";
+ private static final String TABLE = KEYSPACE + ".users";
+
+ @BeforeAll
+ static void startCassandra() {
+ cassandra.start();
+
+ cqlSession = CqlSession.builder()
+ .addContactPoint(new InetSocketAddress("localhost", cassandra.getMappedPort(CASSANDRA_PORT)))
+ .withLocalDatacenter("datacenter1")
+ .build();
+
+ // Setup: call directly on session so it is NOT intercepted by the replacement
+ cqlSession.execute("CREATE KEYSPACE IF NOT EXISTS " + KEYSPACE +
+ " WITH replication = {'class':'SimpleStrategy','replication_factor':1}");
+ cqlSession.execute("CREATE TABLE IF NOT EXISTS " + TABLE +
+ " (id uuid PRIMARY KEY, name text, age int)");
+
+ ExecutionTracer.reset();
+ }
+
+ @AfterAll
+ static void cleanup() {
+ if (cqlSession != null) {
+ cqlSession.close();
+ }
+ ExecutionTracer.reset();
+ }
+
+ @BeforeEach
+ void clearTable() {
+ // Direct call — not intercepted
+ cqlSession.execute("TRUNCATE " + TABLE);
+ ExecutionTracer.reset();
+ }
+
+ @Test
+ void testExecuteSelectIsTracked() {
+ String query = "SELECT * FROM " + TABLE;
+
+ CqlSessionClassReplacement.execute(cqlSession, query);
+
+ List additionalInfoList = ExecutionTracer.exposeAdditionalInfoList();
+ assertEquals(1, additionalInfoList.size());
+
+ Set commands = additionalInfoList.get(0).getCqlInfoData();
+ assertEquals(1, commands.size());
+
+ ExecutedCqlCommand cmd = commands.iterator().next();
+ assertEquals(query, cmd.getCqlCommand());
+ assertFalse(cmd.hasThrownCqlException());
+ assertTrue(cmd.getExecutionTime() >= 0);
+ }
+
+ @Test
+ void testExecuteInsertIsTracked() {
+ String query = "INSERT INTO " + TABLE + " (id, name, age) VALUES (uuid(), 'Alice', 30)";
+
+ CqlSessionClassReplacement.execute(cqlSession, query);
+
+ List additionalInfoList = ExecutionTracer.exposeAdditionalInfoList();
+ assertEquals(1, additionalInfoList.size());
+
+ Set commands = additionalInfoList.get(0).getCqlInfoData();
+ assertEquals(1, commands.size());
+
+ ExecutedCqlCommand cmd = commands.iterator().next();
+ assertEquals(query, cmd.getCqlCommand());
+ assertFalse(cmd.hasThrownCqlException());
+ assertTrue(cmd.getExecutionTime() >= 0);
+ }
+
+ @Test
+ void testMultipleExecutionsAreAllTracked() {
+ String insert = "INSERT INTO " + TABLE + " (id, name, age) VALUES (uuid(), 'Bob', 25)";
+ String select = "SELECT * FROM " + TABLE;
+
+ CqlSessionClassReplacement.execute(cqlSession, insert);
+ CqlSessionClassReplacement.execute(cqlSession, select);
+
+ List additionalInfoList = ExecutionTracer.exposeAdditionalInfoList();
+ assertEquals(1, additionalInfoList.size());
+
+ Set commands = additionalInfoList.get(0).getCqlInfoData();
+ assertEquals(2, commands.size());
+ }
+
+ @Test
+ void testDirectCallsAreNotTracked() {
+ // Calls that bypass the replacement must not appear in the tracker
+ cqlSession.execute("INSERT INTO " + TABLE + " (id, name, age) VALUES (uuid(), 'Carol', 20)");
+
+ List additionalInfoList = ExecutionTracer.exposeAdditionalInfoList();
+ assertEquals(1, additionalInfoList.size());
+ assertTrue(additionalInfoList.get(0).getCqlInfoData().isEmpty());
+ }
+
+ @Test
+ void testExecutingInitCassandraFlagSuppressesTracking() {
+ ExecutionTracer.setExecutingInitCassandra(true);
+ try {
+ CqlSessionClassReplacement.execute(cqlSession, "SELECT * FROM " + TABLE);
+ } finally {
+ ExecutionTracer.setExecutingInitCassandra(false);
+ }
+
+ List additionalInfoList = ExecutionTracer.exposeAdditionalInfoList();
+ assertEquals(1, additionalInfoList.size());
+ assertTrue(additionalInfoList.get(0).getCqlInfoData().isEmpty());
+ }
+}
diff --git a/core/src/main/kotlin/org/evomaster/core/EMConfig.kt b/core/src/main/kotlin/org/evomaster/core/EMConfig.kt
index 1bc461ea74..9a90c6590f 100644
--- a/core/src/main/kotlin/org/evomaster/core/EMConfig.kt
+++ b/core/src/main/kotlin/org/evomaster/core/EMConfig.kt
@@ -1990,6 +1990,12 @@ class EMConfig {
" on the JVM.")
var instrumentMR_MONGO = true
+ @Cfg("Execute instrumentation for method replace with category CASSANDRA." +
+ " Note: this applies only for languages in which instrumentation is applied at runtime, like Java/Kotlin" +
+ " on the JVM.")
+ @Experimental
+ var instrumentMR_CASSANDRA = false
+
@Cfg("Execute instrumentation for method replace with category DYNAMODB." +
" Note: this applies only for languages in which instrumentation is applied at runtime, like Java/Kotlin" +
" on the JVM.")
@@ -3192,6 +3198,7 @@ class EMConfig {
if (instrumentMR_EXT_0) categories.add(ReplacementCategory.EXT_0.toString())
if (instrumentMR_NET) categories.add(ReplacementCategory.NET.toString())
if (instrumentMR_MONGO) categories.add(ReplacementCategory.MONGO.toString())
+ if (instrumentMR_CASSANDRA) categories.add(ReplacementCategory.CASSANDRA.toString())
if (instrumentMR_OPENSEARCH) categories.add(ReplacementCategory.OPENSEARCH.toString())
if (instrumentMR_REDIS) categories.add(ReplacementCategory.REDIS.toString())
if (instrumentMR_DYNAMODB) categories.add(ReplacementCategory.DYNAMODB.toString())
diff --git a/pom.xml b/pom.xml
index 7b2e860bcc..2da1ff2e20 100644
--- a/pom.xml
+++ b/pom.xml
@@ -153,6 +153,7 @@
3.1.0
3.1.0
4.2.3
+ 4.19.2
1.19.0
3.1.0
1.0.0
@@ -600,6 +601,24 @@
${org.mongodb.version}
+
+ org.apache.cassandra
+ java-driver-core
+ ${cassandra.driver.version}
+
+
+
+ org.apache.cassandra
+ java-driver-query-builder
+ ${cassandra.driver.version}
+
+
+
+ org.apache.cassandra
+ java-driver-mapper-runtime
+ ${cassandra.driver.version}
+
+
io.lettuce