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