From 82530a6bf8ca6bf1e215a4f1b186c50e1c2c4582 Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Mon, 6 Jul 2026 19:50:24 -0300 Subject: [PATCH 1/8] Add instrumentation of MappingCassandraEntityInformationClass --- client-java/instrumentation/pom.xml | 6 ++ .../java/instrumentation/AdditionalInfo.java | 10 +++ .../instrumentation/CassandraTableSchema.java | 46 +++++++++++ .../methodreplacement/ReplacementList.java | 1 + ...ndraEntityInformationClassReplacement.java | 82 +++++++++++++++++++ .../staticstate/ExecutionTracer.java | 6 ++ .../MappingCassandraEntityOperationsImpl.java | 13 +++ ...appingCassandraEntityInstrumentedTest.java | 60 ++++++++++++++ .../MappingCassandraEntityOperations.java | 9 ++ 9 files changed, 233 insertions(+) create mode 100644 client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/CassandraTableSchema.java create mode 100644 client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/MappingCassandraEntityInformationClassReplacement.java create mode 100644 client-java/instrumentation/src/test/java/com/foo/somedifferentpackage/examples/methodreplacement/MappingCassandraEntityOperationsImpl.java create mode 100644 client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/example/cassandra/MappingCassandraEntityInstrumentedTest.java create mode 100644 client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/example/cassandra/MappingCassandraEntityOperations.java diff --git a/client-java/instrumentation/pom.xml b/client-java/instrumentation/pom.xml index a4c57817c7..740a310982 100644 --- a/client-java/instrumentation/pom.xml +++ b/client-java/instrumentation/pom.xml @@ -67,6 +67,12 @@ ${springboot.version} test + + org.springframework.boot + spring-boot-starter-data-cassandra + ${springboot.version} + test + org.springframework.boot spring-boot-starter-data-redis 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 9493cfb078..cb04f623f9 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 @@ -118,6 +118,8 @@ public StatementDescription(String line, String method) { private final Set mongoCollectionSchemaData = new CopyOnWriteArraySet<>(); + private final Set cassandraTableSchemaData = new CopyOnWriteArraySet<>(); + public Set getSqlInfoData(){ return Collections.unmodifiableSet(executedSqlCommandData); } @@ -150,6 +152,10 @@ public Set getMongoCollectionTypeData(){ return Collections.unmodifiableSet(mongoCollectionSchemaData); } + public Set getCassandraTableTypeData(){ + return Collections.unmodifiableSet(cassandraTableSchemaData); + } + public void addSqlInfo(ExecutedSqlCommand info){ executedSqlCommandData.add(info); } @@ -182,6 +188,10 @@ public void addMongoCollectionType(MongoCollectionSchema mongoCollectionSchema){ mongoCollectionSchemaData.add(mongoCollectionSchema); } + public void addCassandraTableType(CassandraTableSchema cassandraTableSchema){ + cassandraTableSchemaData.add(cassandraTableSchema); + } + public Set getParsedDtoNamesView(){ return Collections.unmodifiableSet(parsedDtoNames); } diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/CassandraTableSchema.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/CassandraTableSchema.java new file mode 100644 index 0000000000..e854b35da3 --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/CassandraTableSchema.java @@ -0,0 +1,46 @@ +package org.evomaster.client.java.instrumentation; + +import java.io.Serializable; +import java.util.Objects; + +/** + * Schema of rows in a Cassandra table. + */ +public class CassandraTableSchema implements Serializable { + private final String tableName; + private final String tableSchema; + + public CassandraTableSchema(String tableName, String tableSchema) { + this.tableName = tableName; + this.tableSchema = tableSchema; + } + + public String getTableName() { + return tableName; + } + + public String getTableSchema() { + return tableSchema; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CassandraTableSchema that = (CassandraTableSchema) o; + return Objects.equals(tableName, that.tableName) && Objects.equals(tableSchema, that.tableSchema); + } + + @Override + public int hashCode() { + return Objects.hash(tableName, tableSchema); + } + + @Override + public String toString() { + return "CassandraTableSchema{" + + "tableName='" + tableName + '\'' + + ", tableSchema='" + tableSchema + '\'' + + '}'; + } +} \ No newline at end of file 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 24f2b9d3e2..6a6e74a7c3 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 @@ -54,6 +54,7 @@ public static List getList() { new LocalTimeClassReplacement(), new LongClassReplacement(), new MapClassReplacement(), + new MappingCassandraEntityInformationClassReplacement(), new MatcherClassReplacement(), new MessageBodyReaderClassReplacement(), new MethodClassReplacement(), diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/MappingCassandraEntityInformationClassReplacement.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/MappingCassandraEntityInformationClassReplacement.java new file mode 100644 index 0000000000..752b6ec38f --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/MappingCassandraEntityInformationClassReplacement.java @@ -0,0 +1,82 @@ +package org.evomaster.client.java.instrumentation.coverage.methodreplacement.thirdpartyclasses; + +import org.evomaster.client.java.instrumentation.CassandraTableSchema; +import org.evomaster.client.java.instrumentation.coverage.methodreplacement.Replacement; +import org.evomaster.client.java.instrumentation.coverage.methodreplacement.ThirdPartyCast; +import org.evomaster.client.java.instrumentation.coverage.methodreplacement.ThirdPartyMethodReplacementClass; +import org.evomaster.client.java.instrumentation.object.ClassToSchema; +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.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * When a Cassandra repository is created in Spring, a CqlSession is used under the hood. + * But information about the type of the repository's rows is not transferred to the session. + * That info is retained on Spring side. + * So the intention of this replacement is to retrieve that type info. + * This will allow us to create and insert rows of the correct type in the table (and the repository). + */ +public class MappingCassandraEntityInformationClassReplacement extends ThirdPartyMethodReplacementClass { + private static final MappingCassandraEntityInformationClassReplacement singleton = new MappingCassandraEntityInformationClassReplacement(); + private static ThreadLocal instance = new ThreadLocal<>(); + + @Override + protected String getNameOfThirdPartyTargetClass() { + return "org.springframework.data.cassandra.repository.support.MappingCassandraEntityInformation"; + } + + public static Object consumeInstance() { + Object mappingCassandraEntityInformation = instance.get(); + if (mappingCassandraEntityInformation == null) { + throw new IllegalStateException("No instance to consume"); + } + instance.set(null); + return mappingCassandraEntityInformation; + } + + private static void addInstance(Object x) { + Object mappingCassandraEntityInformation = instance.get(); + if (mappingCassandraEntityInformation != null) { + throw new IllegalStateException("Previous instance was not consumed"); + } + instance.set(x); + } + + @Replacement( + replacingConstructor = true, + type = ReplacementType.TRACKER, + category = ReplacementCategory.CASSANDRA, + id = "constructorEntityConverter", + castTo = "org.springframework.data.cassandra.repository.support.MappingCassandraEntityInformation" + ) + public static void MappingCassandraEntityInformation( + @ThirdPartyCast(actualType = "org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity") Object entity, + @ThirdPartyCast(actualType = "org.springframework.data.cassandra.core.convert.CassandraConverter") Object converter) { + handleMappingCassandraEntityInformationConstructor("constructorEntityConverter", Arrays.asList(entity, converter)); + } + + private static void handleMappingCassandraEntityInformationConstructor(String id, List args) { + Constructor original = getOriginalConstructor(singleton, id); + + try { + Object mappingCassandraEntityInformation = original.newInstance(args.toArray()); + addInstance(mappingCassandraEntityInformation); + + Object tableNameId = mappingCassandraEntityInformation.getClass().getMethod("getTableName").invoke(mappingCassandraEntityInformation); + String tableName = (String) tableNameId.getClass().getMethod("asInternal").invoke(tableNameId); + + Class rowType = (Class) mappingCassandraEntityInformation.getClass().getMethod("getJavaType").invoke(mappingCassandraEntityInformation); + String schema = ClassToSchema.getOrDeriveSchemaWithItsRef(rowType, true, Collections.emptyList()); + + ExecutionTracer.addCassandraTableType(new CassandraTableSchema(tableName, schema)); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file 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 2386227c12..7df7c68478 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 @@ -478,6 +478,12 @@ public static void addMongoCollectionType(MongoCollectionSchema mongoCollectionS } } + public static void addCassandraTableType(CassandraTableSchema cassandraTableSchema){ + if (!executingInitCassandra) { + getCurrentAdditionalInfo().addCassandraTableType(cassandraTableSchema); + } + } + public static void markLastExecutedStatement(String lastLine, String lastMethod) { getCurrentAdditionalInfo().pushLastExecutedStatement(lastLine, lastMethod); diff --git a/client-java/instrumentation/src/test/java/com/foo/somedifferentpackage/examples/methodreplacement/MappingCassandraEntityOperationsImpl.java b/client-java/instrumentation/src/test/java/com/foo/somedifferentpackage/examples/methodreplacement/MappingCassandraEntityOperationsImpl.java new file mode 100644 index 0000000000..ac2361084b --- /dev/null +++ b/client-java/instrumentation/src/test/java/com/foo/somedifferentpackage/examples/methodreplacement/MappingCassandraEntityOperationsImpl.java @@ -0,0 +1,13 @@ +package com.foo.somedifferentpackage.examples.methodreplacement; + +import org.evomaster.client.java.instrumentation.example.cassandra.MappingCassandraEntityOperations; +import org.springframework.data.cassandra.core.convert.CassandraConverter; +import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity; +import org.springframework.data.cassandra.repository.support.MappingCassandraEntityInformation; + +public class MappingCassandraEntityOperationsImpl implements MappingCassandraEntityOperations { + @Override + public MappingCassandraEntityInformation callMappingCassandraEntityInformation(CassandraPersistentEntity entity, CassandraConverter converter) { + return new MappingCassandraEntityInformation<>(entity, converter); + } +} \ No newline at end of file diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/example/cassandra/MappingCassandraEntityInstrumentedTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/example/cassandra/MappingCassandraEntityInstrumentedTest.java new file mode 100644 index 0000000000..2e6b15bda7 --- /dev/null +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/example/cassandra/MappingCassandraEntityInstrumentedTest.java @@ -0,0 +1,60 @@ +package org.evomaster.client.java.instrumentation.example.cassandra; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.foo.somedifferentpackage.examples.methodreplacement.MappingCassandraEntityOperationsImpl; +import org.evomaster.client.java.instrumentation.InstrumentingClassLoader; +import org.evomaster.client.java.instrumentation.staticstate.ExecutionTracer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.data.cassandra.core.convert.CassandraConverter; +import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity; +import org.springframework.data.cassandra.repository.support.MappingCassandraEntityInformation; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +public class MappingCassandraEntityInstrumentedTest { + + public static class SomeType { + public String someField; + } + + protected MappingCassandraEntityOperations getInstance() throws Exception { + + InstrumentingClassLoader cl = new InstrumentingClassLoader("com.foo"); + + return (MappingCassandraEntityOperations) + cl.loadClass(MappingCassandraEntityOperationsImpl.class.getName()) + .newInstance(); + } + + @BeforeEach + public void init(){ + ExecutionTracer.reset(); + assertEquals(0 , ExecutionTracer.getNumberOfObjectives()); + } + + @AfterEach + public void checkInstrumentation(){ + assertTrue(ExecutionTracer.getNumberOfObjectives() > 0); + } + + @SuppressWarnings("unchecked") + @Test + public void testConstructor() throws Exception { + MappingCassandraEntityOperations cassandraInstrumented = getInstance(); + + CassandraPersistentEntity entity = mock(CassandraPersistentEntity.class); + when(entity.getType()).thenReturn(SomeType.class); + when(entity.getTableName()).thenReturn(CqlIdentifier.fromCql("some_table")); + + CassandraConverter converter = mock(CassandraConverter.class); + + MappingCassandraEntityInformation mappingCassandraEntityInformation = + cassandraInstrumented.callMappingCassandraEntityInformation(entity, converter); + assertNotNull(mappingCassandraEntityInformation); + } +} \ No newline at end of file diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/example/cassandra/MappingCassandraEntityOperations.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/example/cassandra/MappingCassandraEntityOperations.java new file mode 100644 index 0000000000..138ff458bd --- /dev/null +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/example/cassandra/MappingCassandraEntityOperations.java @@ -0,0 +1,9 @@ +package org.evomaster.client.java.instrumentation.example.cassandra; + +import org.springframework.data.cassandra.core.convert.CassandraConverter; +import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity; +import org.springframework.data.cassandra.repository.support.MappingCassandraEntityInformation; + +public interface MappingCassandraEntityOperations { + MappingCassandraEntityInformation callMappingCassandraEntityInformation(CassandraPersistentEntity entity, CassandraConverter converter); +} \ No newline at end of file From 88180f745c3e4cec2909743b10c8d6bba5636333 Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Mon, 6 Jul 2026 20:08:15 -0300 Subject: [PATCH 2/8] Inline method --- ...ingCassandraEntityInformationClassReplacement.java | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/MappingCassandraEntityInformationClassReplacement.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/MappingCassandraEntityInformationClassReplacement.java index 752b6ec38f..e9a10551c1 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/MappingCassandraEntityInformationClassReplacement.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/MappingCassandraEntityInformationClassReplacement.java @@ -11,9 +11,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; -import java.util.Arrays; import java.util.Collections; -import java.util.List; /** * When a Cassandra repository is created in Spring, a CqlSession is used under the hood. @@ -58,14 +56,10 @@ private static void addInstance(Object x) { public static void MappingCassandraEntityInformation( @ThirdPartyCast(actualType = "org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity") Object entity, @ThirdPartyCast(actualType = "org.springframework.data.cassandra.core.convert.CassandraConverter") Object converter) { - handleMappingCassandraEntityInformationConstructor("constructorEntityConverter", Arrays.asList(entity, converter)); - } - - private static void handleMappingCassandraEntityInformationConstructor(String id, List args) { - Constructor original = getOriginalConstructor(singleton, id); + Constructor original = getOriginalConstructor(singleton, "constructorEntityConverter"); try { - Object mappingCassandraEntityInformation = original.newInstance(args.toArray()); + Object mappingCassandraEntityInformation = original.newInstance(entity, converter); addInstance(mappingCassandraEntityInformation); Object tableNameId = mappingCassandraEntityInformation.getClass().getMethod("getTableName").invoke(mappingCassandraEntityInformation); @@ -79,4 +73,5 @@ private static void handleMappingCassandraEntityInformationConstructor(String id throw new RuntimeException(e); } } + } \ No newline at end of file From 3b209dca800e1c1f9da013b5f8442096051c4ff0 Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Tue, 21 Jul 2026 00:33:36 -0300 Subject: [PATCH 3/8] Add CassandraTemplateClassReplacement --- .../methodreplacement/ReplacementList.java | 1 + .../CassandraTemplateClassReplacement.java | 107 +++++++++++ ...CassandraTemplateClassReplacementTest.java | 170 ++++++++++++++++++ .../CassandraTemplateTestDto.java | 25 +++ 4 files changed, 303 insertions(+) create mode 100644 client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateClassReplacement.java create mode 100644 client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateClassReplacementTest.java create mode 100644 client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateTestDto.java 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 6a6e74a7c3..0c2b87b038 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 @@ -29,6 +29,7 @@ public static List getList() { new Base64DecoderClassReplacement(), new BooleanClassReplacement(), new ByteClassReplacement(), + new CassandraTemplateClassReplacement(), new CharacterClassReplacement(), new CollectionClassReplacement(), new CqlSessionClassReplacement(), diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateClassReplacement.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateClassReplacement.java new file mode 100644 index 0000000000..1da1d8c223 --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateClassReplacement.java @@ -0,0 +1,107 @@ +package org.evomaster.client.java.instrumentation.coverage.methodreplacement.thirdpartyclasses; + +import org.evomaster.client.java.instrumentation.CassandraTableSchema; +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.object.ClassToSchema; +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; +import java.util.Collections; +import java.util.List; + +/** + * The intention of this replacement is the same as {@link MappingCassandraEntityInformationClassReplacement}: + * retrieve the entity-type-to-table mapping. But that constructor replacement only fires when Spring Data + * instantiates a {@code CassandraRepository}; a SUT calling {@code CassandraTemplate} directly (bypassing + * the repository layer) needs this replacement to have that mapping recorded too. + */ +public class CassandraTemplateClassReplacement extends ThirdPartyMethodReplacementClass { + + private static final CassandraTemplateClassReplacement singleton = new CassandraTemplateClassReplacement(); + + @Override + protected String getNameOfThirdPartyTargetClass() { + return "org.springframework.data.cassandra.core.CassandraTemplate"; + } + + private static final String INSERT_ID = "insert"; + + @Replacement(replacingStatic = false, + type = ReplacementType.TRACKER, + id = INSERT_ID, + usageFilter = UsageFilter.ANY, + category = ReplacementCategory.CASSANDRA) + public static T insert(Object cassandraTemplate, T entity) { + try { + addCassandraTableType(cassandraTemplate, entity.getClass()); + + Method insertMethod = getOriginal(singleton, INSERT_ID, cassandraTemplate); + Object result = insertMethod.invoke(cassandraTemplate, entity); + return (T) result; + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw (RuntimeException) e.getCause(); + } + } + + private static final String SELECT_ONE_ID = "selectOneString"; + + @Replacement(replacingStatic = false, + type = ReplacementType.TRACKER, + id = SELECT_ONE_ID, + usageFilter = UsageFilter.ANY, + category = ReplacementCategory.CASSANDRA) + public static T selectOne(Object cassandraTemplate, String cql, Class entityClass) { + try { + addCassandraTableType(cassandraTemplate, entityClass); + + Method selectOneMethod = getOriginal(singleton, SELECT_ONE_ID, cassandraTemplate); + Object result = selectOneMethod.invoke(cassandraTemplate, cql, entityClass); + return (T) result; + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw (RuntimeException) e.getCause(); + } + } + + private static final String SELECT_ID = "selectString"; + + @Replacement(replacingStatic = false, + type = ReplacementType.TRACKER, + id = SELECT_ID, + usageFilter = UsageFilter.ANY, + category = ReplacementCategory.CASSANDRA) + public static List select(Object cassandraTemplate, String cql, Class entityClass) { + try { + addCassandraTableType(cassandraTemplate, entityClass); + + Method selectMethod = getOriginal(singleton, SELECT_ID, cassandraTemplate); + Object result = selectMethod.invoke(cassandraTemplate, cql, entityClass); + return (List) result; + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw (RuntimeException) e.getCause(); + } + } + + private static void addCassandraTableType(Object cassandraTemplate, Class entityClass) { + try { + Object tableNameId = cassandraTemplate.getClass().getMethod("getTableName", Class.class) + .invoke(cassandraTemplate, entityClass); + String tableName = (String) tableNameId.getClass().getMethod("asInternal").invoke(tableNameId); + + String schema = ClassToSchema.getOrDeriveSchemaWithItsRef(entityClass, true, Collections.emptyList()); + ExecutionTracer.addCassandraTableType(new CassandraTableSchema(tableName, schema)); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateClassReplacementTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateClassReplacementTest.java new file mode 100644 index 0000000000..3134281b7a --- /dev/null +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateClassReplacementTest.java @@ -0,0 +1,170 @@ +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.CassandraTableSchema; +import org.evomaster.client.java.instrumentation.object.ClassToSchema; +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.springframework.data.cassandra.core.CassandraTemplate; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +import java.net.InetSocketAddress; +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +public class CassandraTemplateClassReplacementTest { + + private static CqlSession cqlSession; + private static CassandraTemplate cassandraTemplate; + + 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_NAME = "cassandra_template_test_dto"; + private static final String HOST_NAME = "localhost"; + private static final String DATA_CENTER = "datacenter1"; + + @BeforeAll + static void startCassandra() { + cassandra.start(); + + InetSocketAddress contactPoint = + new InetSocketAddress(HOST_NAME, cassandra.getMappedPort(CASSANDRA_PORT)); + + // Bootstrap session, only to create the keyspace (which doesn't exist yet, so we + // can't bind to it as a default keyspace until after this point) + try (CqlSession bootstrap = CqlSession.builder() + .addContactPoint(contactPoint) + .withLocalDatacenter(DATA_CENTER) + .build()) { + bootstrap.execute("CREATE KEYSPACE IF NOT EXISTS " + KEYSPACE + + " WITH replication = {'class':'SimpleStrategy','replication_factor':1}"); + } + + // CassandraTemplate's convenience methods (insert/select/selectOneById) build CQL + // using just the unqualified table name, so the session needs a default keyspace. + cqlSession = CqlSession.builder() + .addContactPoint(contactPoint) + .withLocalDatacenter(DATA_CENTER) + .withKeyspace(KEYSPACE) + .build(); + + // Setup: call directly on session so it is NOT intercepted by any replacement + cqlSession.execute("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + + " (id uuid PRIMARY KEY, name text, age int)"); + + cassandraTemplate = new CassandraTemplate(cqlSession); + + ExecutionTracer.reset(); + } + + @AfterAll + static void cleanup() { + if (cqlSession != null) { + cqlSession.close(); + } + ExecutionTracer.reset(); + } + + @BeforeEach + void clearTable() { + // Direct call — not intercepted + cqlSession.execute("TRUNCATE " + TABLE_NAME); + ExecutionTracer.reset(); + } + + private static String expectedSchema() { + return ClassToSchema.getOrDeriveSchemaWithItsRef(CassandraTemplateTestDto.class, true, Collections.emptyList()); + } + + private static void assertSingleRecordedTableType() { + List additionalInfoList = ExecutionTracer.exposeAdditionalInfoList(); + assertEquals(1, additionalInfoList.size()); + + Set tableTypes = additionalInfoList.get(0).getCassandraTableTypeData(); + assertEquals(1, tableTypes.size()); + + CassandraTableSchema tableSchema = tableTypes.iterator().next(); + assertEquals(TABLE_NAME, tableSchema.getTableName()); + assertEquals(expectedSchema(), tableSchema.getTableSchema()); + } + + @Test + void testInsert() { + UUID id = UUID.randomUUID(); + CassandraTemplateTestDto dto = new CassandraTemplateTestDto(id, "Alice", 30); + + CassandraTemplateClassReplacement.insert(cassandraTemplate, dto); + + // verify the row was actually persisted, via a direct (untracked) query + CassandraTemplateTestDto persisted = + cassandraTemplate.selectOneById(id, CassandraTemplateTestDto.class); + assertNotNull(persisted); + assertEquals("Alice", persisted.name); + assertEquals(30, persisted.age); + + assertSingleRecordedTableType(); + } + + @Test + void testSelectOne() { + UUID id = UUID.randomUUID(); + // Direct insert — not intercepted + cqlSession.execute("INSERT INTO " + TABLE_NAME + " (id, name, age) VALUES (" + id + ", 'Bob', 25)"); + + CassandraTemplateTestDto retrieved = CassandraTemplateClassReplacement.selectOne( + cassandraTemplate, "SELECT * FROM " + TABLE_NAME + " WHERE id = " + id, CassandraTemplateTestDto.class); + + assertNotNull(retrieved); + assertEquals("Bob", retrieved.name); + assertEquals(25, retrieved.age); + + assertSingleRecordedTableType(); + } + + @Test + void testSelect() { + UUID id1 = UUID.randomUUID(); + UUID id2 = UUID.randomUUID(); + // Direct inserts — not intercepted + cqlSession.execute("INSERT INTO " + TABLE_NAME + " (id, name, age) VALUES (" + id1 + ", 'Carol', 40)"); + cqlSession.execute("INSERT INTO " + TABLE_NAME + " (id, name, age) VALUES (" + id2 + ", 'Dave', 45)"); + + List retrieved = CassandraTemplateClassReplacement.select( + cassandraTemplate, "SELECT * FROM " + TABLE_NAME, CassandraTemplateTestDto.class); + + assertEquals(2, retrieved.size()); + + assertSingleRecordedTableType(); + } + + @Test + void testDirectCallsAreNotTracked() { + UUID id = UUID.randomUUID(); + CassandraTemplateTestDto dto = new CassandraTemplateTestDto(id, "Erin", 22); + + // Calls that bypass the replacement must not appear in the tracker + cassandraTemplate.insert(dto); + + List additionalInfoList = ExecutionTracer.exposeAdditionalInfoList(); + assertEquals(1, additionalInfoList.size()); + assertTrue(additionalInfoList.get(0).getCassandraTableTypeData().isEmpty()); + } +} \ No newline at end of file diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateTestDto.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateTestDto.java new file mode 100644 index 0000000000..d5fca7c10a --- /dev/null +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateTestDto.java @@ -0,0 +1,25 @@ +package org.evomaster.client.java.instrumentation.coverage.methodreplacement.thirdpartyclasses; + +import org.springframework.data.cassandra.core.mapping.PrimaryKey; +import org.springframework.data.cassandra.core.mapping.Table; + +import java.util.UUID; + +@Table("cassandra_template_test_dto") +public class CassandraTemplateTestDto { + + @PrimaryKey + public UUID id; + + public String name; + public int age; + + public CassandraTemplateTestDto() { + } + + public CassandraTemplateTestDto(UUID id, String name, int age) { + this.id = id; + this.name = name; + this.age = age; + } +} From dba41e9908d043de48204ffc4cd3d782a5bcca23 Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Tue, 21 Jul 2026 18:19:06 -0300 Subject: [PATCH 4/8] Reorganise constants --- .../CassandraTemplateClassReplacement.java | 6 ++---- .../MappingCassandraEntityInformationClassReplacement.java | 6 ++++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateClassReplacement.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateClassReplacement.java index 1da1d8c223..cf670f711e 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateClassReplacement.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateClassReplacement.java @@ -30,6 +30,8 @@ protected String getNameOfThirdPartyTargetClass() { } private static final String INSERT_ID = "insert"; + private static final String SELECT_ONE_ID = "selectOneString"; + private static final String SELECT_ID = "selectString"; @Replacement(replacingStatic = false, type = ReplacementType.TRACKER, @@ -50,8 +52,6 @@ public static T insert(Object cassandraTemplate, T entity) { } } - private static final String SELECT_ONE_ID = "selectOneString"; - @Replacement(replacingStatic = false, type = ReplacementType.TRACKER, id = SELECT_ONE_ID, @@ -71,8 +71,6 @@ public static T selectOne(Object cassandraTemplate, String cql, Class ent } } - private static final String SELECT_ID = "selectString"; - @Replacement(replacingStatic = false, type = ReplacementType.TRACKER, id = SELECT_ID, diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/MappingCassandraEntityInformationClassReplacement.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/MappingCassandraEntityInformationClassReplacement.java index e9a10551c1..980206c230 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/MappingCassandraEntityInformationClassReplacement.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/MappingCassandraEntityInformationClassReplacement.java @@ -24,6 +24,8 @@ public class MappingCassandraEntityInformationClassReplacement extends ThirdPart private static final MappingCassandraEntityInformationClassReplacement singleton = new MappingCassandraEntityInformationClassReplacement(); private static ThreadLocal instance = new ThreadLocal<>(); + public static final String CONSTRUCTOR_ENTITY_CONVERTER_ID = "constructorEntityConverter"; + @Override protected String getNameOfThirdPartyTargetClass() { return "org.springframework.data.cassandra.repository.support.MappingCassandraEntityInformation"; @@ -50,13 +52,13 @@ private static void addInstance(Object x) { replacingConstructor = true, type = ReplacementType.TRACKER, category = ReplacementCategory.CASSANDRA, - id = "constructorEntityConverter", + id = CONSTRUCTOR_ENTITY_CONVERTER_ID, castTo = "org.springframework.data.cassandra.repository.support.MappingCassandraEntityInformation" ) public static void MappingCassandraEntityInformation( @ThirdPartyCast(actualType = "org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity") Object entity, @ThirdPartyCast(actualType = "org.springframework.data.cassandra.core.convert.CassandraConverter") Object converter) { - Constructor original = getOriginalConstructor(singleton, "constructorEntityConverter"); + Constructor original = getOriginalConstructor(singleton, CONSTRUCTOR_ENTITY_CONVERTER_ID); try { Object mappingCassandraEntityInformation = original.newInstance(entity, converter); From 815184b83a8392d64a9d79daf88aeebefbcff75f Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Mon, 27 Jul 2026 18:44:43 -0300 Subject: [PATCH 5/8] Trace table metadata directly from driver --- client-java/instrumentation/pom.xml | 6 - .../java/instrumentation/AdditionalInfo.java | 11 +- .../instrumentation/CassandraTableSchema.java | 46 ---- .../cassandra/CassandraColumnMetadata.java | 55 ++++ .../cassandra/CassandraSchemaTracer.java | 187 ++++++++++++++ .../cassandra/CassandraTableMetadata.java | 61 +++++ .../methodreplacement/ReplacementList.java | 2 - .../CassandraTemplateClassReplacement.java | 105 -------- .../CqlSessionClassReplacement.java | 47 +++- ...ndraEntityInformationClassReplacement.java | 79 ------ .../staticstate/ExecutionTracer.java | 8 +- .../MappingCassandraEntityOperationsImpl.java | 13 - .../cassandra/CassandraSchemaTracerTest.java | 237 ++++++++++++++++++ ...CassandraTemplateClassReplacementTest.java | 170 ------------- .../CassandraTemplateTestDto.java | 25 -- ...appingCassandraEntityInstrumentedTest.java | 60 ----- .../MappingCassandraEntityOperations.java | 9 - 17 files changed, 590 insertions(+), 531 deletions(-) delete mode 100644 client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/CassandraTableSchema.java create mode 100644 client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CassandraColumnMetadata.java create mode 100644 client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CassandraSchemaTracer.java create mode 100644 client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CassandraTableMetadata.java delete mode 100644 client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateClassReplacement.java delete mode 100644 client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/MappingCassandraEntityInformationClassReplacement.java delete mode 100644 client-java/instrumentation/src/test/java/com/foo/somedifferentpackage/examples/methodreplacement/MappingCassandraEntityOperationsImpl.java create mode 100644 client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/cassandra/CassandraSchemaTracerTest.java delete mode 100644 client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateClassReplacementTest.java delete mode 100644 client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateTestDto.java delete mode 100644 client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/example/cassandra/MappingCassandraEntityInstrumentedTest.java delete mode 100644 client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/example/cassandra/MappingCassandraEntityOperations.java diff --git a/client-java/instrumentation/pom.xml b/client-java/instrumentation/pom.xml index 393daed83d..961c2f818b 100644 --- a/client-java/instrumentation/pom.xml +++ b/client-java/instrumentation/pom.xml @@ -67,12 +67,6 @@ ${springboot.version} test - - org.springframework.boot - spring-boot-starter-data-cassandra - ${springboot.version} - test - org.springframework.boot spring-boot-starter-data-redis 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 cb04f623f9..72aba7a97e 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 @@ -1,5 +1,6 @@ package org.evomaster.client.java.instrumentation; +import org.evomaster.client.java.instrumentation.cassandra.CassandraTableMetadata; import org.evomaster.client.java.instrumentation.shared.StringSpecializationInfo; import org.evomaster.client.java.instrumentation.staticstate.ExecutionTracer; import org.evomaster.client.java.utils.SimpleLogger; @@ -118,7 +119,7 @@ public StatementDescription(String line, String method) { private final Set mongoCollectionSchemaData = new CopyOnWriteArraySet<>(); - private final Set cassandraTableSchemaData = new CopyOnWriteArraySet<>(); + private final Set cassandraTableMetadataData = new CopyOnWriteArraySet<>(); public Set getSqlInfoData(){ return Collections.unmodifiableSet(executedSqlCommandData); @@ -152,8 +153,8 @@ public Set getMongoCollectionTypeData(){ return Collections.unmodifiableSet(mongoCollectionSchemaData); } - public Set getCassandraTableTypeData(){ - return Collections.unmodifiableSet(cassandraTableSchemaData); + public Set getCassandraTableMetadataData(){ + return Collections.unmodifiableSet(cassandraTableMetadataData); } public void addSqlInfo(ExecutedSqlCommand info){ @@ -188,8 +189,8 @@ public void addMongoCollectionType(MongoCollectionSchema mongoCollectionSchema){ mongoCollectionSchemaData.add(mongoCollectionSchema); } - public void addCassandraTableType(CassandraTableSchema cassandraTableSchema){ - cassandraTableSchemaData.add(cassandraTableSchema); + public void addCassandraTableMetadata(CassandraTableMetadata cassandraTableMetadata){ + cassandraTableMetadataData.add(cassandraTableMetadata); } public Set getParsedDtoNamesView(){ diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/CassandraTableSchema.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/CassandraTableSchema.java deleted file mode 100644 index e854b35da3..0000000000 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/CassandraTableSchema.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.evomaster.client.java.instrumentation; - -import java.io.Serializable; -import java.util.Objects; - -/** - * Schema of rows in a Cassandra table. - */ -public class CassandraTableSchema implements Serializable { - private final String tableName; - private final String tableSchema; - - public CassandraTableSchema(String tableName, String tableSchema) { - this.tableName = tableName; - this.tableSchema = tableSchema; - } - - public String getTableName() { - return tableName; - } - - public String getTableSchema() { - return tableSchema; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - CassandraTableSchema that = (CassandraTableSchema) o; - return Objects.equals(tableName, that.tableName) && Objects.equals(tableSchema, that.tableSchema); - } - - @Override - public int hashCode() { - return Objects.hash(tableName, tableSchema); - } - - @Override - public String toString() { - return "CassandraTableSchema{" + - "tableName='" + tableName + '\'' + - ", tableSchema='" + tableSchema + '\'' + - '}'; - } -} \ No newline at end of file diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CassandraColumnMetadata.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CassandraColumnMetadata.java new file mode 100644 index 0000000000..33cf71ffd7 --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CassandraColumnMetadata.java @@ -0,0 +1,55 @@ +package org.evomaster.client.java.instrumentation.cassandra; + +import java.io.Serializable; +import java.util.Objects; + +/** + * A single column of a Cassandra table, as captured from the driver's table metadata: its name, + * CQL type, and whether it belongs to the partition key or is a clustering column. + */ +public class CassandraColumnMetadata implements Serializable { + + private final String name; + private final String cqlType; + private final boolean partitionKey; + private final boolean clusteringColumn; + + public CassandraColumnMetadata(String name, String cqlType, boolean partitionKey, boolean clusteringColumn) { + this.name = name; + this.cqlType = cqlType; + this.partitionKey = partitionKey; + this.clusteringColumn = clusteringColumn; + } + + public String getName() { + return name; + } + + public String getCqlType() { + return cqlType; + } + + public boolean isPartitionKey() { + return partitionKey; + } + + public boolean isClusteringColumn() { + return clusteringColumn; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof CassandraColumnMetadata)) return false; + CassandraColumnMetadata that = (CassandraColumnMetadata) o; + return partitionKey == that.partitionKey + && clusteringColumn == that.clusteringColumn + && Objects.equals(name, that.name) + && Objects.equals(cqlType, that.cqlType); + } + + @Override + public int hashCode() { + return Objects.hash(name, cqlType, partitionKey, clusteringColumn); + } +} \ No newline at end of file diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CassandraSchemaTracer.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CassandraSchemaTracer.java new file mode 100644 index 0000000000..58eca2b1e0 --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CassandraSchemaTracer.java @@ -0,0 +1,187 @@ +package org.evomaster.client.java.instrumentation.cassandra; + +import org.evomaster.client.java.instrumentation.staticstate.ExecutionTracer; + +import java.lang.reflect.Method; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Caches Cassandra table schemas read directly from the driver's own metadata + * ({@code CqlSession.getMetadata()}), rather than from Spring Data hooks. This works regardless of + * whether the SUT talks to Cassandra through Spring Data repositories, {@code CassandraTemplate}, + * or a bare {@code CqlSession}, since the driver always carries this information. + *

+ * {@link #resolve(Object, String, String)} is called from + * {@link org.evomaster.client.java.instrumentation.coverage.methodreplacement.thirdpartyclasses.CqlSessionClassReplacement} + * right after every successfully executed CQL statement, so the cache is populated as a side effect + * of real SUT traffic: a table's shape is fetched once, the first time it's referenced, traced via + * {@link ExecutionTracer#addCassandraTableMetadata(CassandraTableMetadata)}, and served straight + * from the cache (without re-tracing it) on every later reference, since {@code TableMetadata} is + * never partial (a single lookup already returns every column of a table). + *

+ * The cache is held in a static field rather than an injected instance: it mirrors + * {@link org.evomaster.client.java.instrumentation.object.ClassToSchema}'s cache, and needs to be + * reachable from the static replacement methods on {@code CqlSessionClassReplacement}. + */ +public class CassandraSchemaTracer { + + private static final String CQL_IDENTIFIER_CLASS = "com.datastax.oss.driver.api.core.CqlIdentifier"; + + /** + * Key -> the captured shape of a keyspace's table. + * Value -> its columns, types, and primary-key roles, as last read from the driver. + */ + private static final Map tables = new ConcurrentHashMap<>(); + + private CassandraSchemaTracer() { + } + + /** + * Resets the tracer's status + */ + public static void reset() { + tables.clear(); + } + + /** + * Resolves the shape of a keyspace/table. Tables seen before are served straight from the + * cache; a table not seen before (e.g. queried for the first time) is fetched once and cached. + * + * @param cqlSession a live {@code com.datastax.oss.driver.api.core.CqlSession}, accessed + * purely via reflection so this module has no compile-time driver dependency + * @param keyspaceNameRaw the raw, quote-preserving keyspace text as it appeared in the CQL + * statement, or {@code null} if the statement didn't qualify the table + * with a keyspace (the session's current keyspace applies instead) + * @param tableNameRaw the raw, quote-preserving table text as it appeared in the CQL statement + * @return the table's shape, or {@code null} if the keyspace/table can't be resolved + * (unqualified reference with no current keyspace set, or the keyspace/table doesn't exist) + */ + public static CassandraTableMetadata resolve(Object cqlSession, String keyspaceNameRaw, String tableNameRaw) { + try { + Object keyspaceIdentifier = keyspaceNameRaw != null + ? cqlIdentifierFromCql(keyspaceNameRaw) + : currentKeyspaceIdentifier(cqlSession); + if (keyspaceIdentifier == null) { + return null; + } + + Object metadata = invoke(cqlSession, "getMetadata"); + Object keyspaceMetadata = unwrapOptional(invokeWithCqlIdentifier(metadata, "getKeyspace", keyspaceIdentifier)); + if (keyspaceMetadata == null) { + return null; + } + + Object tableIdentifier = cqlIdentifierFromCql(tableNameRaw); + Object tableMetadataObj = unwrapOptional(invokeWithCqlIdentifier(keyspaceMetadata, "getTable", tableIdentifier)); + if (tableMetadataObj == null) { + return null; + } + + String keyspaceName = asInternal(invoke(keyspaceMetadata, "getName")); + String tableName = asInternal(invoke(tableMetadataObj, "getName")); + TableKey key = new TableKey(keyspaceName, tableName); + + // fast path: skip re-walking columns/partition-key/clustering-columns on a cache hit, + // since this is called on every intercepted query + CassandraTableMetadata cached = tables.get(key); + if (cached != null) { + return cached; + } + + CassandraTableMetadata tableMetadata = buildTableMetadata(keyspaceName, tableMetadataObj); + tables.put(key, tableMetadata); + // first time this table is seen: trace its schema, mirroring how it used to be captured + // from Spring Data + ExecutionTracer.addCassandraTableMetadata(tableMetadata); + return tableMetadata; + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Failed to resolve Cassandra table metadata for " + + keyspaceNameRaw + "." + tableNameRaw, e); + } + } + + private static CassandraTableMetadata buildTableMetadata(String keyspaceName, Object tableMetadataObj) throws ReflectiveOperationException { + String tableName = asInternal(invoke(tableMetadataObj, "getName")); + + Set partitionKeyNames = namesOf((List) invoke(tableMetadataObj, "getPartitionKey")); + Set clusteringNames = namesOf(((Map) invoke(tableMetadataObj, "getClusteringColumns")).keySet()); + + Map columnsByName = (Map) invoke(tableMetadataObj, "getColumns"); + List columns = new ArrayList<>(); + for (Object columnMetadataObj : columnsByName.values()) { + String columnName = asInternal(invoke(columnMetadataObj, "getName")); + String cqlType = asCql(invoke(columnMetadataObj, "getType")); + columns.add(new CassandraColumnMetadata(columnName, cqlType, + partitionKeyNames.contains(columnName), clusteringNames.contains(columnName))); + } + return new CassandraTableMetadata(keyspaceName, tableName, columns); + } + + private static Set namesOf(Iterable columnMetadataObjs) throws ReflectiveOperationException { + Set names = new HashSet<>(); + for (Object columnMetadataObj : columnMetadataObjs) { + names.add(asInternal(invoke(columnMetadataObj, "getName"))); + } + return names; + } + + private static Object currentKeyspaceIdentifier(Object cqlSession) throws ReflectiveOperationException { + return unwrapOptional(invoke(cqlSession, "getKeyspace")); + } + + private static Object cqlIdentifierFromCql(String rawText) throws ReflectiveOperationException { + Class cqlIdentifierClass = Class.forName(CQL_IDENTIFIER_CLASS); + return cqlIdentifierClass.getMethod("fromCql", String.class).invoke(null, rawText); + } + + private static Object invokeWithCqlIdentifier(Object target, String methodName, Object cqlIdentifierArg) throws ReflectiveOperationException { + Class cqlIdentifierClass = Class.forName(CQL_IDENTIFIER_CLASS); + Method method = target.getClass().getMethod(methodName, cqlIdentifierClass); + return method.invoke(target, cqlIdentifierArg); + } + + private static Object unwrapOptional(Object optional) throws ReflectiveOperationException { + boolean present = (boolean) invoke(optional, "isPresent"); + return present ? invoke(optional, "get") : null; + } + + private static String asInternal(Object cqlIdentifier) throws ReflectiveOperationException { + return (String) invoke(cqlIdentifier, "asInternal"); + } + + private static String asCql(Object dataType) throws ReflectiveOperationException { + return (String) dataType.getClass().getMethod("asCql", boolean.class, boolean.class).invoke(dataType, false, false); + } + + private static Object invoke(Object target, String methodName) throws ReflectiveOperationException { + return target.getClass().getMethod(methodName).invoke(target); + } + + /** + * Cache key: a table's resolved (never null) keyspace and table name, in the driver's + * canonical/internal form. + */ + private static class TableKey { + private final String keyspaceName; + private final String tableName; + + TableKey(String keyspaceName, String tableName) { + this.keyspaceName = keyspaceName; + this.tableName = tableName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof TableKey)) return false; + TableKey tableKey = (TableKey) o; + return Objects.equals(keyspaceName, tableKey.keyspaceName) && Objects.equals(tableName, tableKey.tableName); + } + + @Override + public int hashCode() { + return Objects.hash(keyspaceName, tableName); + } + } +} \ No newline at end of file diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CassandraTableMetadata.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CassandraTableMetadata.java new file mode 100644 index 0000000000..232d1fa684 --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CassandraTableMetadata.java @@ -0,0 +1,61 @@ +package org.evomaster.client.java.instrumentation.cassandra; + +import java.io.Serializable; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * The captured shape of a Cassandra table: its full column list, with CQL types and + * partition-key/clustering-column roles, as read once from the driver's table metadata. + */ +public class CassandraTableMetadata implements Serializable { + + private final String keyspaceName; + private final String tableName; + private final List columns; + + public CassandraTableMetadata(String keyspaceName, String tableName, List columns) { + this.keyspaceName = keyspaceName; + this.tableName = tableName; + this.columns = Collections.unmodifiableList(columns); + } + + public String getKeyspaceName() { + return keyspaceName; + } + + public String getTableName() { + return tableName; + } + + public List getColumns() { + return columns; + } + + /** + * Structural equality, ignoring column order: two captures of the same table are considered + * equal if they have the same columns with the same types and primary-key roles, regardless + * of the (driver-internal, not CQL-meaningful) iteration order they were read in. + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof CassandraTableMetadata)) return false; + CassandraTableMetadata that = (CassandraTableMetadata) o; + return Objects.equals(keyspaceName, that.keyspaceName) + && Objects.equals(tableName, that.tableName) + && Objects.equals(asSet(columns), asSet(that.columns)); + } + + @Override + public int hashCode() { + return Objects.hash(keyspaceName, tableName, asSet(columns)); + } + + private static Set asSet(List columns) { + return new HashSet<>(columns); + } +} \ No newline at end of file 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 0c2b87b038..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 @@ -29,7 +29,6 @@ public static List getList() { new Base64DecoderClassReplacement(), new BooleanClassReplacement(), new ByteClassReplacement(), - new CassandraTemplateClassReplacement(), new CharacterClassReplacement(), new CollectionClassReplacement(), new CqlSessionClassReplacement(), @@ -55,7 +54,6 @@ public static List getList() { new LocalTimeClassReplacement(), new LongClassReplacement(), new MapClassReplacement(), - new MappingCassandraEntityInformationClassReplacement(), new MatcherClassReplacement(), new MessageBodyReaderClassReplacement(), new MethodClassReplacement(), diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateClassReplacement.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateClassReplacement.java deleted file mode 100644 index cf670f711e..0000000000 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateClassReplacement.java +++ /dev/null @@ -1,105 +0,0 @@ -package org.evomaster.client.java.instrumentation.coverage.methodreplacement.thirdpartyclasses; - -import org.evomaster.client.java.instrumentation.CassandraTableSchema; -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.object.ClassToSchema; -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; -import java.util.Collections; -import java.util.List; - -/** - * The intention of this replacement is the same as {@link MappingCassandraEntityInformationClassReplacement}: - * retrieve the entity-type-to-table mapping. But that constructor replacement only fires when Spring Data - * instantiates a {@code CassandraRepository}; a SUT calling {@code CassandraTemplate} directly (bypassing - * the repository layer) needs this replacement to have that mapping recorded too. - */ -public class CassandraTemplateClassReplacement extends ThirdPartyMethodReplacementClass { - - private static final CassandraTemplateClassReplacement singleton = new CassandraTemplateClassReplacement(); - - @Override - protected String getNameOfThirdPartyTargetClass() { - return "org.springframework.data.cassandra.core.CassandraTemplate"; - } - - private static final String INSERT_ID = "insert"; - private static final String SELECT_ONE_ID = "selectOneString"; - private static final String SELECT_ID = "selectString"; - - @Replacement(replacingStatic = false, - type = ReplacementType.TRACKER, - id = INSERT_ID, - usageFilter = UsageFilter.ANY, - category = ReplacementCategory.CASSANDRA) - public static T insert(Object cassandraTemplate, T entity) { - try { - addCassandraTableType(cassandraTemplate, entity.getClass()); - - Method insertMethod = getOriginal(singleton, INSERT_ID, cassandraTemplate); - Object result = insertMethod.invoke(cassandraTemplate, entity); - return (T) result; - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } catch (InvocationTargetException e) { - throw (RuntimeException) e.getCause(); - } - } - - @Replacement(replacingStatic = false, - type = ReplacementType.TRACKER, - id = SELECT_ONE_ID, - usageFilter = UsageFilter.ANY, - category = ReplacementCategory.CASSANDRA) - public static T selectOne(Object cassandraTemplate, String cql, Class entityClass) { - try { - addCassandraTableType(cassandraTemplate, entityClass); - - Method selectOneMethod = getOriginal(singleton, SELECT_ONE_ID, cassandraTemplate); - Object result = selectOneMethod.invoke(cassandraTemplate, cql, entityClass); - return (T) result; - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } catch (InvocationTargetException e) { - throw (RuntimeException) e.getCause(); - } - } - - @Replacement(replacingStatic = false, - type = ReplacementType.TRACKER, - id = SELECT_ID, - usageFilter = UsageFilter.ANY, - category = ReplacementCategory.CASSANDRA) - public static List select(Object cassandraTemplate, String cql, Class entityClass) { - try { - addCassandraTableType(cassandraTemplate, entityClass); - - Method selectMethod = getOriginal(singleton, SELECT_ID, cassandraTemplate); - Object result = selectMethod.invoke(cassandraTemplate, cql, entityClass); - return (List) result; - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } catch (InvocationTargetException e) { - throw (RuntimeException) e.getCause(); - } - } - - private static void addCassandraTableType(Object cassandraTemplate, Class entityClass) { - try { - Object tableNameId = cassandraTemplate.getClass().getMethod("getTableName", Class.class) - .invoke(cassandraTemplate, entityClass); - String tableName = (String) tableNameId.getClass().getMethod("asInternal").invoke(tableNameId); - - String schema = ClassToSchema.getOrDeriveSchemaWithItsRef(entityClass, true, Collections.emptyList()); - ExecutionTracer.addCassandraTableType(new CassandraTableSchema(tableName, schema)); - } catch (ReflectiveOperationException e) { - throw new RuntimeException(e); - } - } -} \ No newline at end of file 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 index 59a2bbd771..bd9d01b44a 100644 --- 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 @@ -1,6 +1,7 @@ package org.evomaster.client.java.instrumentation.coverage.methodreplacement.thirdpartyclasses; import org.evomaster.client.java.instrumentation.ExecutedCqlCommand; +import org.evomaster.client.java.instrumentation.cassandra.CassandraSchemaTracer; import org.evomaster.client.java.instrumentation.coverage.methodreplacement.Replacement; import org.evomaster.client.java.instrumentation.coverage.methodreplacement.ThirdPartyCast; import org.evomaster.client.java.instrumentation.coverage.methodreplacement.ThirdPartyMethodReplacementClass; @@ -8,6 +9,7 @@ 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 org.evomaster.client.java.utils.SimpleLogger; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -76,6 +78,9 @@ private static Object handleCqlExecute(String id, Object cqlSession, String quer long end = System.currentTimeMillis(); long executionTime = end - start; TableReference ref = extractTableReference(queryForTracking); + if (ref.rawTableName != null) { + captureTableSchema(cqlSession, ref); + } ExecutedCqlCommand info = new ExecutedCqlCommand(queryForTracking, ref.keyspaceName, ref.tableName, false, executionTime); ExecutionTracer.addCqlInfo(info); return result; @@ -86,6 +91,21 @@ private static Object handleCqlExecute(String id, Object cqlSession, String quer } } + /** + * Best-effort: caches the queried table's schema, read directly from the driver's own + * metadata, so it's available later without depending on Spring Data. Uses the raw, + * quote-preserving keyspace/table text (not the lower-cased, quote-stripped fields on + * {@link TableReference} used for {@link ExecutedCqlCommand}), since CQL treats quoted + * identifiers as case-sensitive. Never lets a schema-capture failure break the SUT's own query. + */ + private static void captureTableSchema(Object cqlSession, TableReference ref) { + try { + CassandraSchemaTracer.resolve(cqlSession, ref.rawKeyspaceName, ref.rawTableName); + } catch (RuntimeException e) { + SimpleLogger.uniqueWarn("Failed to capture Cassandra schema for table " + ref.rawTableName); + } + } + /** * Best-effort extraction of keyspace/table from the CQL text. Returns both fields as * null when the query doesn't match a recognised SELECT/INSERT/UPDATE/DELETE shape @@ -93,18 +113,22 @@ private static Object handleCqlExecute(String id, Object cqlSession, String quer */ private static TableReference extractTableReference(String query) { if (query == null) { - return new TableReference(null, null); + return new TableReference(null, null, null, null); } Matcher matcher = TABLE_REFERENCE_PATTERN.matcher(query); if (!matcher.find()) { - return new TableReference(null, null); + return new TableReference(null, null, null, null); } else { - String first = stripQuotes(matcher.group("first")); - String second = matcher.group("second") != null ? stripQuotes(matcher.group("second")) : null; + String rawFirst = matcher.group("first"); + String rawSecond = matcher.group("second"); + String first = stripQuotes(rawFirst); + String second = rawSecond != null ? stripQuotes(rawSecond) : null; // if there is an "a.b" qualifier, "a" is the keyspace and "b" is the table; // otherwise the single identifier is the table, and the keyspace is the session default - return second != null ? new TableReference(first, second) : new TableReference(null, first); + return second != null + ? new TableReference(first, second, rawFirst, rawSecond) + : new TableReference(null, first, null, rawFirst); } } @@ -119,10 +143,19 @@ private static String stripQuotes(String identifier) { private static class TableReference { final String keyspaceName; final String tableName; - - TableReference(String keyspaceName, String tableName) { + /** + * Same keyspace/table, but as the raw, quote-preserving text matched in the CQL string + * (not lower-cased/quote-stripped), needed to resolve the table correctly against the + * driver's own (quote-sensitive) metadata. + */ + final String rawKeyspaceName; + final String rawTableName; + + TableReference(String keyspaceName, String tableName, String rawKeyspaceName, String rawTableName) { this.keyspaceName = keyspaceName; this.tableName = tableName; + this.rawKeyspaceName = rawKeyspaceName; + this.rawTableName = rawTableName; } } diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/MappingCassandraEntityInformationClassReplacement.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/MappingCassandraEntityInformationClassReplacement.java deleted file mode 100644 index 980206c230..0000000000 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/MappingCassandraEntityInformationClassReplacement.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.evomaster.client.java.instrumentation.coverage.methodreplacement.thirdpartyclasses; - -import org.evomaster.client.java.instrumentation.CassandraTableSchema; -import org.evomaster.client.java.instrumentation.coverage.methodreplacement.Replacement; -import org.evomaster.client.java.instrumentation.coverage.methodreplacement.ThirdPartyCast; -import org.evomaster.client.java.instrumentation.coverage.methodreplacement.ThirdPartyMethodReplacementClass; -import org.evomaster.client.java.instrumentation.object.ClassToSchema; -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.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.util.Collections; - -/** - * When a Cassandra repository is created in Spring, a CqlSession is used under the hood. - * But information about the type of the repository's rows is not transferred to the session. - * That info is retained on Spring side. - * So the intention of this replacement is to retrieve that type info. - * This will allow us to create and insert rows of the correct type in the table (and the repository). - */ -public class MappingCassandraEntityInformationClassReplacement extends ThirdPartyMethodReplacementClass { - private static final MappingCassandraEntityInformationClassReplacement singleton = new MappingCassandraEntityInformationClassReplacement(); - private static ThreadLocal instance = new ThreadLocal<>(); - - public static final String CONSTRUCTOR_ENTITY_CONVERTER_ID = "constructorEntityConverter"; - - @Override - protected String getNameOfThirdPartyTargetClass() { - return "org.springframework.data.cassandra.repository.support.MappingCassandraEntityInformation"; - } - - public static Object consumeInstance() { - Object mappingCassandraEntityInformation = instance.get(); - if (mappingCassandraEntityInformation == null) { - throw new IllegalStateException("No instance to consume"); - } - instance.set(null); - return mappingCassandraEntityInformation; - } - - private static void addInstance(Object x) { - Object mappingCassandraEntityInformation = instance.get(); - if (mappingCassandraEntityInformation != null) { - throw new IllegalStateException("Previous instance was not consumed"); - } - instance.set(x); - } - - @Replacement( - replacingConstructor = true, - type = ReplacementType.TRACKER, - category = ReplacementCategory.CASSANDRA, - id = CONSTRUCTOR_ENTITY_CONVERTER_ID, - castTo = "org.springframework.data.cassandra.repository.support.MappingCassandraEntityInformation" - ) - public static void MappingCassandraEntityInformation( - @ThirdPartyCast(actualType = "org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity") Object entity, - @ThirdPartyCast(actualType = "org.springframework.data.cassandra.core.convert.CassandraConverter") Object converter) { - Constructor original = getOriginalConstructor(singleton, CONSTRUCTOR_ENTITY_CONVERTER_ID); - - try { - Object mappingCassandraEntityInformation = original.newInstance(entity, converter); - addInstance(mappingCassandraEntityInformation); - - Object tableNameId = mappingCassandraEntityInformation.getClass().getMethod("getTableName").invoke(mappingCassandraEntityInformation); - String tableName = (String) tableNameId.getClass().getMethod("asInternal").invoke(tableNameId); - - Class rowType = (Class) mappingCassandraEntityInformation.getClass().getMethod("getJavaType").invoke(mappingCassandraEntityInformation); - String schema = ClassToSchema.getOrDeriveSchemaWithItsRef(rowType, true, Collections.emptyList()); - - ExecutionTracer.addCassandraTableType(new CassandraTableSchema(tableName, schema)); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { - throw new RuntimeException(e); - } - } - -} \ No newline at end of file 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 7df7c68478..259ef9fa9e 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 @@ -1,8 +1,9 @@ package org.evomaster.client.java.instrumentation.staticstate; +import org.evomaster.client.java.distance.heuristics.Truthness; import org.evomaster.client.java.instrumentation.*; +import org.evomaster.client.java.instrumentation.cassandra.CassandraTableMetadata; import org.evomaster.client.java.instrumentation.heuristic.HeuristicsForJumps; -import org.evomaster.client.java.distance.heuristics.Truthness; import org.evomaster.client.java.instrumentation.shared.*; import java.util.*; @@ -478,13 +479,12 @@ public static void addMongoCollectionType(MongoCollectionSchema mongoCollectionS } } - public static void addCassandraTableType(CassandraTableSchema cassandraTableSchema){ + public static void addCassandraTableMetadata(CassandraTableMetadata cassandraTableMetadata){ if (!executingInitCassandra) { - getCurrentAdditionalInfo().addCassandraTableType(cassandraTableSchema); + getCurrentAdditionalInfo().addCassandraTableMetadata(cassandraTableMetadata); } } - public static void markLastExecutedStatement(String lastLine, String lastMethod) { getCurrentAdditionalInfo().pushLastExecutedStatement(lastLine, lastMethod); } diff --git a/client-java/instrumentation/src/test/java/com/foo/somedifferentpackage/examples/methodreplacement/MappingCassandraEntityOperationsImpl.java b/client-java/instrumentation/src/test/java/com/foo/somedifferentpackage/examples/methodreplacement/MappingCassandraEntityOperationsImpl.java deleted file mode 100644 index ac2361084b..0000000000 --- a/client-java/instrumentation/src/test/java/com/foo/somedifferentpackage/examples/methodreplacement/MappingCassandraEntityOperationsImpl.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.foo.somedifferentpackage.examples.methodreplacement; - -import org.evomaster.client.java.instrumentation.example.cassandra.MappingCassandraEntityOperations; -import org.springframework.data.cassandra.core.convert.CassandraConverter; -import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity; -import org.springframework.data.cassandra.repository.support.MappingCassandraEntityInformation; - -public class MappingCassandraEntityOperationsImpl implements MappingCassandraEntityOperations { - @Override - public MappingCassandraEntityInformation callMappingCassandraEntityInformation(CassandraPersistentEntity entity, CassandraConverter converter) { - return new MappingCassandraEntityInformation<>(entity, converter); - } -} \ No newline at end of file diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/cassandra/CassandraSchemaTracerTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/cassandra/CassandraSchemaTracerTest.java new file mode 100644 index 0000000000..b15673dd2d --- /dev/null +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/cassandra/CassandraSchemaTracerTest.java @@ -0,0 +1,237 @@ +package org.evomaster.client.java.instrumentation.cassandra; + +import com.datastax.oss.driver.api.core.CqlSession; +import org.evomaster.client.java.instrumentation.AdditionalInfo; +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 CassandraSchemaTracerTest { + + 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 HOST_NAME = "localhost"; + private static final String DATA_CENTER = "datacenter1"; + + private static final String KEYSPACE_1 = "schema_repo_ks1"; + private static final String KEYSPACE_2 = "schema_repo_ks2"; + + @BeforeAll + static void startCassandra() { + cassandra.start(); + + InetSocketAddress contactPoint = + new InetSocketAddress(HOST_NAME, cassandra.getMappedPort(CASSANDRA_PORT)); + + cqlSession = CqlSession.builder() + .addContactPoint(contactPoint) + .withLocalDatacenter(DATA_CENTER) + .build(); + + cqlSession.execute("CREATE KEYSPACE IF NOT EXISTS " + KEYSPACE_1 + + " WITH replication = {'class':'SimpleStrategy','replication_factor':1}"); + cqlSession.execute("CREATE KEYSPACE IF NOT EXISTS " + KEYSPACE_2 + + " WITH replication = {'class':'SimpleStrategy','replication_factor':1}"); + + // simple single-column primary key + cqlSession.execute("CREATE TABLE IF NOT EXISTS " + KEYSPACE_1 + ".table_a" + + " (id uuid PRIMARY KEY, name text)"); + + // composite partition key + clustering column, to exercise PK/clustering tagging + cqlSession.execute("CREATE TABLE IF NOT EXISTS " + KEYSPACE_2 + ".table_b" + + " (part1 int, part2 int, rank int, payload text, PRIMARY KEY((part1, part2), rank))"); + + // a quoted, mixed-case keyspace/table with a different shape than any lowercase twin + cqlSession.execute("CREATE KEYSPACE IF NOT EXISTS \"" + "SchemaRepoMixedCaseKs" + "\"" + + " WITH replication = {'class':'SimpleStrategy','replication_factor':1}"); + cqlSession.execute("CREATE TABLE IF NOT EXISTS \"SchemaRepoMixedCaseKs\".\"MixedCaseTable\"" + + " (id int PRIMARY KEY, amount int)"); + // a lowercase twin, with a deliberately different column set + cqlSession.execute("CREATE TABLE IF NOT EXISTS " + KEYSPACE_1 + ".mixedcasetable" + + " (id int PRIMARY KEY, unrelated_column text)"); + } + + @AfterAll + static void cleanup() { + if (cqlSession != null) { + cqlSession.close(); + } + ExecutionTracer.reset(); + } + + @BeforeEach + void resetTracer() { + ExecutionTracer.reset(); + } + + private static CassandraColumnMetadata columnNamed(CassandraTableMetadata table, String name) { + return table.getColumns().stream() + .filter(c -> c.getName().equals(name)) + .findFirst() + .orElseThrow(() -> new AssertionError("No column named " + name)); + } + + @Test + void resolve_capturesTablesAcrossMultipleKeyspaces() { + CassandraTableMetadata tableA = CassandraSchemaTracer.resolve(cqlSession, KEYSPACE_1, "table_a"); + CassandraTableMetadata tableB = CassandraSchemaTracer.resolve(cqlSession, KEYSPACE_2, "table_b"); + + assertNotNull(tableA); + assertEquals(KEYSPACE_1, tableA.getKeyspaceName()); + assertEquals("table_a", tableA.getTableName()); + + assertNotNull(tableB); + assertEquals(KEYSPACE_2, tableB.getKeyspaceName()); + assertEquals("table_b", tableB.getTableName()); + } + + @Test + void resolve_taggedPartitionKeyAndClusteringColumns() { + CassandraTableMetadata tableB = CassandraSchemaTracer.resolve(cqlSession, KEYSPACE_2, "table_b"); + assertNotNull(tableB); + + assertTrue(columnNamed(tableB, "part1").isPartitionKey()); + assertTrue(columnNamed(tableB, "part2").isPartitionKey()); + assertFalse(columnNamed(tableB, "part1").isClusteringColumn()); + + assertTrue(columnNamed(tableB, "rank").isClusteringColumn()); + assertFalse(columnNamed(tableB, "rank").isPartitionKey()); + + CassandraColumnMetadata payload = columnNamed(tableB, "payload"); + assertFalse(payload.isPartitionKey()); + assertFalse(payload.isClusteringColumn()); + assertEquals("text", payload.getCqlType()); + } + + @Test + void resolve_unseenTable_capturedOnDemandWithoutError() { + cqlSession.execute("CREATE TABLE IF NOT EXISTS " + KEYSPACE_1 + ".table_created_after_snapshot" + + " (id uuid PRIMARY KEY, age int)"); + + CassandraTableMetadata table = CassandraSchemaTracer.resolve(cqlSession, KEYSPACE_1, "table_created_after_snapshot"); + + assertNotNull(table); + assertEquals("table_created_after_snapshot", table.getTableName()); + assertTrue(columnNamed(table, "id").isPartitionKey()); + assertEquals("int", columnNamed(table, "age").getCqlType()); + } + + @Test + void resolve_unqualifiedReference_usesSessionCurrentKeyspace() { + InetSocketAddress contactPoint = + new InetSocketAddress(HOST_NAME, cassandra.getMappedPort(CASSANDRA_PORT)); + try (CqlSession sessionWithDefaultKeyspace = CqlSession.builder() + .addContactPoint(contactPoint) + .withLocalDatacenter(DATA_CENTER) + .withKeyspace(KEYSPACE_1) + .build()) { + + CassandraTableMetadata table = CassandraSchemaTracer.resolve(sessionWithDefaultKeyspace, null, "table_a"); + + assertNotNull(table); + assertEquals(KEYSPACE_1, table.getKeyspaceName()); + assertEquals("table_a", table.getTableName()); + } + } + + @Test + void resolve_unqualifiedReference_noCurrentKeyspace_returnsNull() { + assertFalse(cqlSession.getKeyspace().isPresent()); + + CassandraTableMetadata table = CassandraSchemaTracer.resolve(cqlSession, null, "table_a"); + + assertNull(table); + } + + @Test + void resolve_nonexistentTable_returnsNull() { + CassandraTableMetadata table = CassandraSchemaTracer.resolve(cqlSession, KEYSPACE_1, "no_such_table"); + + assertNull(table); + } + + @Test + void resolve_nonexistentKeyspace_returnsNull() { + CassandraTableMetadata table = CassandraSchemaTracer.resolve(cqlSession, "no_such_keyspace", "table_a"); + + assertNull(table); + } + + @Test + void resolve_quotedMixedCaseKeyspaceAndTable_targetsCorrectOne() { + CassandraTableMetadata mixedCase = CassandraSchemaTracer.resolve( + cqlSession, "\"SchemaRepoMixedCaseKs\"", "\"MixedCaseTable\""); + + assertNotNull(mixedCase); + assertEquals("SchemaRepoMixedCaseKs", mixedCase.getKeyspaceName()); + assertEquals("MixedCaseTable", mixedCase.getTableName()); + assertNotNull(columnNamed(mixedCase, "amount")); + + // the lowercase, unquoted twin in a different keyspace must not be conflated with it + CassandraTableMetadata lowercaseTwin = CassandraSchemaTracer.resolve(cqlSession, KEYSPACE_1, "mixedcasetable"); + assertNotNull(lowercaseTwin); + assertNotNull(columnNamed(lowercaseTwin, "unrelated_column")); + } + + @Test + void resolve_calledTwiceForSameTable_returnsEqualMetadata() { + CassandraTableMetadata first = CassandraSchemaTracer.resolve(cqlSession, KEYSPACE_1, "table_a"); + CassandraTableMetadata second = CassandraSchemaTracer.resolve(cqlSession, KEYSPACE_1, "table_a"); + + assertNotNull(first); + assertEquals(first, second); + } + + @Test + void resolve_firstTimeSeen_tracesSchemaViaExecutionTracer() { + cqlSession.execute("CREATE TABLE IF NOT EXISTS " + KEYSPACE_1 + ".trace_once_table" + + " (id uuid PRIMARY KEY, age int)"); + + CassandraTableMetadata table = CassandraSchemaTracer.resolve(cqlSession, KEYSPACE_1, "trace_once_table"); + assertNotNull(table); + + List additionalInfoList = ExecutionTracer.exposeAdditionalInfoList(); + assertEquals(1, additionalInfoList.size()); + + Set traced = additionalInfoList.get(0).getCassandraTableMetadataData(); + assertEquals(1, traced.size()); + assertEquals(table, traced.iterator().next()); + } + + @Test + void resolve_alreadyCached_doesNotRetraceSchema() { + cqlSession.execute("CREATE TABLE IF NOT EXISTS " + KEYSPACE_1 + ".trace_no_duplicate_table" + + " (id uuid PRIMARY KEY, age int)"); + + // first call: not yet cached, traces the schema + CassandraSchemaTracer.resolve(cqlSession, KEYSPACE_1, "trace_no_duplicate_table"); + ExecutionTracer.reset(); + + // second call: already cached, must not trace again + CassandraSchemaTracer.resolve(cqlSession, KEYSPACE_1, "trace_no_duplicate_table"); + + List additionalInfoList = ExecutionTracer.exposeAdditionalInfoList(); + assertEquals(1, additionalInfoList.size()); + assertTrue(additionalInfoList.get(0).getCassandraTableMetadataData().isEmpty()); + } +} \ No newline at end of file diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateClassReplacementTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateClassReplacementTest.java deleted file mode 100644 index 3134281b7a..0000000000 --- a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateClassReplacementTest.java +++ /dev/null @@ -1,170 +0,0 @@ -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.CassandraTableSchema; -import org.evomaster.client.java.instrumentation.object.ClassToSchema; -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.springframework.data.cassandra.core.CassandraTemplate; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.wait.strategy.Wait; - -import java.net.InetSocketAddress; -import java.time.Duration; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.*; - -public class CassandraTemplateClassReplacementTest { - - private static CqlSession cqlSession; - private static CassandraTemplate cassandraTemplate; - - 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_NAME = "cassandra_template_test_dto"; - private static final String HOST_NAME = "localhost"; - private static final String DATA_CENTER = "datacenter1"; - - @BeforeAll - static void startCassandra() { - cassandra.start(); - - InetSocketAddress contactPoint = - new InetSocketAddress(HOST_NAME, cassandra.getMappedPort(CASSANDRA_PORT)); - - // Bootstrap session, only to create the keyspace (which doesn't exist yet, so we - // can't bind to it as a default keyspace until after this point) - try (CqlSession bootstrap = CqlSession.builder() - .addContactPoint(contactPoint) - .withLocalDatacenter(DATA_CENTER) - .build()) { - bootstrap.execute("CREATE KEYSPACE IF NOT EXISTS " + KEYSPACE + - " WITH replication = {'class':'SimpleStrategy','replication_factor':1}"); - } - - // CassandraTemplate's convenience methods (insert/select/selectOneById) build CQL - // using just the unqualified table name, so the session needs a default keyspace. - cqlSession = CqlSession.builder() - .addContactPoint(contactPoint) - .withLocalDatacenter(DATA_CENTER) - .withKeyspace(KEYSPACE) - .build(); - - // Setup: call directly on session so it is NOT intercepted by any replacement - cqlSession.execute("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + - " (id uuid PRIMARY KEY, name text, age int)"); - - cassandraTemplate = new CassandraTemplate(cqlSession); - - ExecutionTracer.reset(); - } - - @AfterAll - static void cleanup() { - if (cqlSession != null) { - cqlSession.close(); - } - ExecutionTracer.reset(); - } - - @BeforeEach - void clearTable() { - // Direct call — not intercepted - cqlSession.execute("TRUNCATE " + TABLE_NAME); - ExecutionTracer.reset(); - } - - private static String expectedSchema() { - return ClassToSchema.getOrDeriveSchemaWithItsRef(CassandraTemplateTestDto.class, true, Collections.emptyList()); - } - - private static void assertSingleRecordedTableType() { - List additionalInfoList = ExecutionTracer.exposeAdditionalInfoList(); - assertEquals(1, additionalInfoList.size()); - - Set tableTypes = additionalInfoList.get(0).getCassandraTableTypeData(); - assertEquals(1, tableTypes.size()); - - CassandraTableSchema tableSchema = tableTypes.iterator().next(); - assertEquals(TABLE_NAME, tableSchema.getTableName()); - assertEquals(expectedSchema(), tableSchema.getTableSchema()); - } - - @Test - void testInsert() { - UUID id = UUID.randomUUID(); - CassandraTemplateTestDto dto = new CassandraTemplateTestDto(id, "Alice", 30); - - CassandraTemplateClassReplacement.insert(cassandraTemplate, dto); - - // verify the row was actually persisted, via a direct (untracked) query - CassandraTemplateTestDto persisted = - cassandraTemplate.selectOneById(id, CassandraTemplateTestDto.class); - assertNotNull(persisted); - assertEquals("Alice", persisted.name); - assertEquals(30, persisted.age); - - assertSingleRecordedTableType(); - } - - @Test - void testSelectOne() { - UUID id = UUID.randomUUID(); - // Direct insert — not intercepted - cqlSession.execute("INSERT INTO " + TABLE_NAME + " (id, name, age) VALUES (" + id + ", 'Bob', 25)"); - - CassandraTemplateTestDto retrieved = CassandraTemplateClassReplacement.selectOne( - cassandraTemplate, "SELECT * FROM " + TABLE_NAME + " WHERE id = " + id, CassandraTemplateTestDto.class); - - assertNotNull(retrieved); - assertEquals("Bob", retrieved.name); - assertEquals(25, retrieved.age); - - assertSingleRecordedTableType(); - } - - @Test - void testSelect() { - UUID id1 = UUID.randomUUID(); - UUID id2 = UUID.randomUUID(); - // Direct inserts — not intercepted - cqlSession.execute("INSERT INTO " + TABLE_NAME + " (id, name, age) VALUES (" + id1 + ", 'Carol', 40)"); - cqlSession.execute("INSERT INTO " + TABLE_NAME + " (id, name, age) VALUES (" + id2 + ", 'Dave', 45)"); - - List retrieved = CassandraTemplateClassReplacement.select( - cassandraTemplate, "SELECT * FROM " + TABLE_NAME, CassandraTemplateTestDto.class); - - assertEquals(2, retrieved.size()); - - assertSingleRecordedTableType(); - } - - @Test - void testDirectCallsAreNotTracked() { - UUID id = UUID.randomUUID(); - CassandraTemplateTestDto dto = new CassandraTemplateTestDto(id, "Erin", 22); - - // Calls that bypass the replacement must not appear in the tracker - cassandraTemplate.insert(dto); - - List additionalInfoList = ExecutionTracer.exposeAdditionalInfoList(); - assertEquals(1, additionalInfoList.size()); - assertTrue(additionalInfoList.get(0).getCassandraTableTypeData().isEmpty()); - } -} \ No newline at end of file diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateTestDto.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateTestDto.java deleted file mode 100644 index d5fca7c10a..0000000000 --- a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CassandraTemplateTestDto.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.evomaster.client.java.instrumentation.coverage.methodreplacement.thirdpartyclasses; - -import org.springframework.data.cassandra.core.mapping.PrimaryKey; -import org.springframework.data.cassandra.core.mapping.Table; - -import java.util.UUID; - -@Table("cassandra_template_test_dto") -public class CassandraTemplateTestDto { - - @PrimaryKey - public UUID id; - - public String name; - public int age; - - public CassandraTemplateTestDto() { - } - - public CassandraTemplateTestDto(UUID id, String name, int age) { - this.id = id; - this.name = name; - this.age = age; - } -} diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/example/cassandra/MappingCassandraEntityInstrumentedTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/example/cassandra/MappingCassandraEntityInstrumentedTest.java deleted file mode 100644 index 2e6b15bda7..0000000000 --- a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/example/cassandra/MappingCassandraEntityInstrumentedTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.evomaster.client.java.instrumentation.example.cassandra; - -import com.datastax.oss.driver.api.core.CqlIdentifier; -import com.foo.somedifferentpackage.examples.methodreplacement.MappingCassandraEntityOperationsImpl; -import org.evomaster.client.java.instrumentation.InstrumentingClassLoader; -import org.evomaster.client.java.instrumentation.staticstate.ExecutionTracer; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.data.cassandra.core.convert.CassandraConverter; -import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity; -import org.springframework.data.cassandra.repository.support.MappingCassandraEntityInformation; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - - -public class MappingCassandraEntityInstrumentedTest { - - public static class SomeType { - public String someField; - } - - protected MappingCassandraEntityOperations getInstance() throws Exception { - - InstrumentingClassLoader cl = new InstrumentingClassLoader("com.foo"); - - return (MappingCassandraEntityOperations) - cl.loadClass(MappingCassandraEntityOperationsImpl.class.getName()) - .newInstance(); - } - - @BeforeEach - public void init(){ - ExecutionTracer.reset(); - assertEquals(0 , ExecutionTracer.getNumberOfObjectives()); - } - - @AfterEach - public void checkInstrumentation(){ - assertTrue(ExecutionTracer.getNumberOfObjectives() > 0); - } - - @SuppressWarnings("unchecked") - @Test - public void testConstructor() throws Exception { - MappingCassandraEntityOperations cassandraInstrumented = getInstance(); - - CassandraPersistentEntity entity = mock(CassandraPersistentEntity.class); - when(entity.getType()).thenReturn(SomeType.class); - when(entity.getTableName()).thenReturn(CqlIdentifier.fromCql("some_table")); - - CassandraConverter converter = mock(CassandraConverter.class); - - MappingCassandraEntityInformation mappingCassandraEntityInformation = - cassandraInstrumented.callMappingCassandraEntityInformation(entity, converter); - assertNotNull(mappingCassandraEntityInformation); - } -} \ No newline at end of file diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/example/cassandra/MappingCassandraEntityOperations.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/example/cassandra/MappingCassandraEntityOperations.java deleted file mode 100644 index 138ff458bd..0000000000 --- a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/example/cassandra/MappingCassandraEntityOperations.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.evomaster.client.java.instrumentation.example.cassandra; - -import org.springframework.data.cassandra.core.convert.CassandraConverter; -import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity; -import org.springframework.data.cassandra.repository.support.MappingCassandraEntityInformation; - -public interface MappingCassandraEntityOperations { - MappingCassandraEntityInformation callMappingCassandraEntityInformation(CassandraPersistentEntity entity, CassandraConverter converter); -} \ No newline at end of file From d6917469f030a02f056b5d9081b7e99bcdda8e29 Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Tue, 28 Jul 2026 17:06:47 -0300 Subject: [PATCH 6/8] PR review changes --- .../cassandra/CassandraSchemaTracer.java | 53 ++++++++++++------- .../CqlSessionClassReplacement.java | 2 +- 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CassandraSchemaTracer.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CassandraSchemaTracer.java index 58eca2b1e0..dc3f1aab2a 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CassandraSchemaTracer.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CassandraSchemaTracer.java @@ -28,6 +28,23 @@ public class CassandraSchemaTracer { private static final String CQL_IDENTIFIER_CLASS = "com.datastax.oss.driver.api.core.CqlIdentifier"; + /* + * Names of the driver methods invoked via reflection throughout this class. + */ + private static final String METHOD_GET_METADATA = "getMetadata"; + private static final String METHOD_GET_KEYSPACE = "getKeyspace"; + private static final String METHOD_GET_TABLE = "getTable"; + private static final String METHOD_GET_NAME = "getName"; + private static final String METHOD_GET_TYPE = "getType"; + private static final String METHOD_FROM_CQL = "fromCql"; + private static final String METHOD_IS_PRESENT = "isPresent"; + private static final String METHOD_GET = "get"; + private static final String METHOD_AS_INTERNAL = "asInternal"; + private static final String METHOD_AS_CQL = "asCql"; + private static final String METHOD_GET_PARTITION_KEY = "getPartitionKey"; + private static final String METHOD_GET_CLUSTERING_COLUMNS = "getClusteringColumns"; + private static final String METHOD_GET_COLUMNS = "getColumns"; + /** * Key -> the captured shape of a keyspace's table. * Value -> its columns, types, and primary-key roles, as last read from the driver. @@ -66,20 +83,20 @@ public static CassandraTableMetadata resolve(Object cqlSession, String keyspaceN return null; } - Object metadata = invoke(cqlSession, "getMetadata"); - Object keyspaceMetadata = unwrapOptional(invokeWithCqlIdentifier(metadata, "getKeyspace", keyspaceIdentifier)); + Object metadata = invoke(cqlSession, METHOD_GET_METADATA); + Object keyspaceMetadata = unwrapOptional(invokeWithCqlIdentifier(metadata, METHOD_GET_KEYSPACE, keyspaceIdentifier)); if (keyspaceMetadata == null) { return null; } Object tableIdentifier = cqlIdentifierFromCql(tableNameRaw); - Object tableMetadataObj = unwrapOptional(invokeWithCqlIdentifier(keyspaceMetadata, "getTable", tableIdentifier)); + Object tableMetadataObj = unwrapOptional(invokeWithCqlIdentifier(keyspaceMetadata, METHOD_GET_TABLE, tableIdentifier)); if (tableMetadataObj == null) { return null; } - String keyspaceName = asInternal(invoke(keyspaceMetadata, "getName")); - String tableName = asInternal(invoke(tableMetadataObj, "getName")); + String keyspaceName = asInternal(invoke(keyspaceMetadata, METHOD_GET_NAME)); + String tableName = asInternal(invoke(tableMetadataObj, METHOD_GET_NAME)); TableKey key = new TableKey(keyspaceName, tableName); // fast path: skip re-walking columns/partition-key/clustering-columns on a cache hit, @@ -102,16 +119,16 @@ public static CassandraTableMetadata resolve(Object cqlSession, String keyspaceN } private static CassandraTableMetadata buildTableMetadata(String keyspaceName, Object tableMetadataObj) throws ReflectiveOperationException { - String tableName = asInternal(invoke(tableMetadataObj, "getName")); + String tableName = asInternal(invoke(tableMetadataObj, METHOD_GET_NAME)); - Set partitionKeyNames = namesOf((List) invoke(tableMetadataObj, "getPartitionKey")); - Set clusteringNames = namesOf(((Map) invoke(tableMetadataObj, "getClusteringColumns")).keySet()); + Set partitionKeyNames = namesOf((List) invoke(tableMetadataObj, METHOD_GET_PARTITION_KEY)); + Set clusteringNames = namesOf(((Map) invoke(tableMetadataObj, METHOD_GET_CLUSTERING_COLUMNS)).keySet()); - Map columnsByName = (Map) invoke(tableMetadataObj, "getColumns"); + Map columnsByName = (Map) invoke(tableMetadataObj, METHOD_GET_COLUMNS); List columns = new ArrayList<>(); for (Object columnMetadataObj : columnsByName.values()) { - String columnName = asInternal(invoke(columnMetadataObj, "getName")); - String cqlType = asCql(invoke(columnMetadataObj, "getType")); + String columnName = asInternal(invoke(columnMetadataObj, METHOD_GET_NAME)); + String cqlType = asCql(invoke(columnMetadataObj, METHOD_GET_TYPE)); columns.add(new CassandraColumnMetadata(columnName, cqlType, partitionKeyNames.contains(columnName), clusteringNames.contains(columnName))); } @@ -121,18 +138,18 @@ private static CassandraTableMetadata buildTableMetadata(String keyspaceName, Ob private static Set namesOf(Iterable columnMetadataObjs) throws ReflectiveOperationException { Set names = new HashSet<>(); for (Object columnMetadataObj : columnMetadataObjs) { - names.add(asInternal(invoke(columnMetadataObj, "getName"))); + names.add(asInternal(invoke(columnMetadataObj, METHOD_GET_NAME))); } return names; } private static Object currentKeyspaceIdentifier(Object cqlSession) throws ReflectiveOperationException { - return unwrapOptional(invoke(cqlSession, "getKeyspace")); + return unwrapOptional(invoke(cqlSession, METHOD_GET_KEYSPACE)); } private static Object cqlIdentifierFromCql(String rawText) throws ReflectiveOperationException { Class cqlIdentifierClass = Class.forName(CQL_IDENTIFIER_CLASS); - return cqlIdentifierClass.getMethod("fromCql", String.class).invoke(null, rawText); + return cqlIdentifierClass.getMethod(METHOD_FROM_CQL, String.class).invoke(null, rawText); } private static Object invokeWithCqlIdentifier(Object target, String methodName, Object cqlIdentifierArg) throws ReflectiveOperationException { @@ -142,16 +159,16 @@ private static Object invokeWithCqlIdentifier(Object target, String methodName, } private static Object unwrapOptional(Object optional) throws ReflectiveOperationException { - boolean present = (boolean) invoke(optional, "isPresent"); - return present ? invoke(optional, "get") : null; + boolean present = (boolean) invoke(optional, METHOD_IS_PRESENT); + return present ? invoke(optional, METHOD_GET) : null; } private static String asInternal(Object cqlIdentifier) throws ReflectiveOperationException { - return (String) invoke(cqlIdentifier, "asInternal"); + return (String) invoke(cqlIdentifier, METHOD_AS_INTERNAL); } private static String asCql(Object dataType) throws ReflectiveOperationException { - return (String) dataType.getClass().getMethod("asCql", boolean.class, boolean.class).invoke(dataType, false, false); + return (String) dataType.getClass().getMethod(METHOD_AS_CQL, boolean.class, boolean.class).invoke(dataType, false, false); } private static Object invoke(Object target, String methodName) throws ReflectiveOperationException { 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 index bd9d01b44a..2b86b5194a 100644 --- 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 @@ -113,7 +113,7 @@ private static void captureTableSchema(Object cqlSession, TableReference ref) { */ private static TableReference extractTableReference(String query) { if (query == null) { - return new TableReference(null, null, null, null); + throw new IllegalArgumentException("query cannot be null"); } Matcher matcher = TABLE_REFERENCE_PATTERN.matcher(query); From ccb8560e103afb2dd2c5d384a86a9e8ba990217a Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Tue, 28 Jul 2026 17:10:15 -0300 Subject: [PATCH 7/8] Inline method --- .../cassandra/CassandraTableMetadata.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CassandraTableMetadata.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CassandraTableMetadata.java index 232d1fa684..16a7b96b82 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CassandraTableMetadata.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CassandraTableMetadata.java @@ -5,7 +5,6 @@ import java.util.HashSet; import java.util.List; import java.util.Objects; -import java.util.Set; /** * The captured shape of a Cassandra table: its full column list, with CQL types and @@ -46,16 +45,12 @@ public boolean equals(Object o) { if (!(o instanceof CassandraTableMetadata)) return false; CassandraTableMetadata that = (CassandraTableMetadata) o; return Objects.equals(keyspaceName, that.keyspaceName) - && Objects.equals(tableName, that.tableName) - && Objects.equals(asSet(columns), asSet(that.columns)); + && Objects.equals(tableName, that.tableName) && Objects.equals(new HashSet<>(columns), new HashSet(that.columns)); } @Override public int hashCode() { - return Objects.hash(keyspaceName, tableName, asSet(columns)); + return Objects.hash(keyspaceName, tableName, new HashSet<>(columns)); } - private static Set asSet(List columns) { - return new HashSet<>(columns); - } } \ No newline at end of file From 4a2a502f9aab9eefd1f5dd652585bf1c961346f5 Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Wed, 29 Jul 2026 19:05:53 -0300 Subject: [PATCH 8/8] Add assertions in CassandraTableMetadata constructor --- .../cassandra/CassandraTableMetadata.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CassandraTableMetadata.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CassandraTableMetadata.java index 16a7b96b82..ce82352f38 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CassandraTableMetadata.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CassandraTableMetadata.java @@ -17,11 +17,24 @@ public class CassandraTableMetadata implements Serializable { private final List columns; public CassandraTableMetadata(String keyspaceName, String tableName, List columns) { + Objects.requireNonNull(tableName); + assertNoDuplicateColumns(tableName, columns); + this.keyspaceName = keyspaceName; this.tableName = tableName; this.columns = Collections.unmodifiableList(columns); } + private static void assertNoDuplicateColumns(String tableName, List columns) { + if (duplicateColumns(columns)) { + throw new IllegalArgumentException("Duplicate column name(s) in table metadata for " + tableName); + } + } + + private static boolean duplicateColumns(List columns) { + return columns.stream().map(CassandraColumnMetadata::getName).distinct().count() != columns.size(); + } + public String getKeyspaceName() { return keyspaceName; }