Skip to content

Commit 3b209dc

Browse files
Add CassandraTemplateClassReplacement
1 parent 88180f7 commit 3b209dc

4 files changed

Lines changed: 303 additions & 0 deletions

File tree

client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/ReplacementList.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public static List<MethodReplacementClass> getList() {
2929
new Base64DecoderClassReplacement(),
3030
new BooleanClassReplacement(),
3131
new ByteClassReplacement(),
32+
new CassandraTemplateClassReplacement(),
3233
new CharacterClassReplacement(),
3334
new CollectionClassReplacement(),
3435
new CqlSessionClassReplacement(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package org.evomaster.client.java.instrumentation.coverage.methodreplacement.thirdpartyclasses;
2+
3+
import org.evomaster.client.java.instrumentation.CassandraTableSchema;
4+
import org.evomaster.client.java.instrumentation.coverage.methodreplacement.Replacement;
5+
import org.evomaster.client.java.instrumentation.coverage.methodreplacement.ThirdPartyMethodReplacementClass;
6+
import org.evomaster.client.java.instrumentation.coverage.methodreplacement.UsageFilter;
7+
import org.evomaster.client.java.instrumentation.object.ClassToSchema;
8+
import org.evomaster.client.java.instrumentation.shared.ReplacementCategory;
9+
import org.evomaster.client.java.instrumentation.shared.ReplacementType;
10+
import org.evomaster.client.java.instrumentation.staticstate.ExecutionTracer;
11+
12+
import java.lang.reflect.InvocationTargetException;
13+
import java.lang.reflect.Method;
14+
import java.util.Collections;
15+
import java.util.List;
16+
17+
/**
18+
* The intention of this replacement is the same as {@link MappingCassandraEntityInformationClassReplacement}:
19+
* retrieve the entity-type-to-table mapping. But that constructor replacement only fires when Spring Data
20+
* instantiates a {@code CassandraRepository}; a SUT calling {@code CassandraTemplate} directly (bypassing
21+
* the repository layer) needs this replacement to have that mapping recorded too.
22+
*/
23+
public class CassandraTemplateClassReplacement extends ThirdPartyMethodReplacementClass {
24+
25+
private static final CassandraTemplateClassReplacement singleton = new CassandraTemplateClassReplacement();
26+
27+
@Override
28+
protected String getNameOfThirdPartyTargetClass() {
29+
return "org.springframework.data.cassandra.core.CassandraTemplate";
30+
}
31+
32+
private static final String INSERT_ID = "insert";
33+
34+
@Replacement(replacingStatic = false,
35+
type = ReplacementType.TRACKER,
36+
id = INSERT_ID,
37+
usageFilter = UsageFilter.ANY,
38+
category = ReplacementCategory.CASSANDRA)
39+
public static <T> T insert(Object cassandraTemplate, T entity) {
40+
try {
41+
addCassandraTableType(cassandraTemplate, entity.getClass());
42+
43+
Method insertMethod = getOriginal(singleton, INSERT_ID, cassandraTemplate);
44+
Object result = insertMethod.invoke(cassandraTemplate, entity);
45+
return (T) result;
46+
} catch (IllegalAccessException e) {
47+
throw new RuntimeException(e);
48+
} catch (InvocationTargetException e) {
49+
throw (RuntimeException) e.getCause();
50+
}
51+
}
52+
53+
private static final String SELECT_ONE_ID = "selectOneString";
54+
55+
@Replacement(replacingStatic = false,
56+
type = ReplacementType.TRACKER,
57+
id = SELECT_ONE_ID,
58+
usageFilter = UsageFilter.ANY,
59+
category = ReplacementCategory.CASSANDRA)
60+
public static <T> T selectOne(Object cassandraTemplate, String cql, Class<T> entityClass) {
61+
try {
62+
addCassandraTableType(cassandraTemplate, entityClass);
63+
64+
Method selectOneMethod = getOriginal(singleton, SELECT_ONE_ID, cassandraTemplate);
65+
Object result = selectOneMethod.invoke(cassandraTemplate, cql, entityClass);
66+
return (T) result;
67+
} catch (IllegalAccessException e) {
68+
throw new RuntimeException(e);
69+
} catch (InvocationTargetException e) {
70+
throw (RuntimeException) e.getCause();
71+
}
72+
}
73+
74+
private static final String SELECT_ID = "selectString";
75+
76+
@Replacement(replacingStatic = false,
77+
type = ReplacementType.TRACKER,
78+
id = SELECT_ID,
79+
usageFilter = UsageFilter.ANY,
80+
category = ReplacementCategory.CASSANDRA)
81+
public static <T> List<T> select(Object cassandraTemplate, String cql, Class<T> entityClass) {
82+
try {
83+
addCassandraTableType(cassandraTemplate, entityClass);
84+
85+
Method selectMethod = getOriginal(singleton, SELECT_ID, cassandraTemplate);
86+
Object result = selectMethod.invoke(cassandraTemplate, cql, entityClass);
87+
return (List<T>) result;
88+
} catch (IllegalAccessException e) {
89+
throw new RuntimeException(e);
90+
} catch (InvocationTargetException e) {
91+
throw (RuntimeException) e.getCause();
92+
}
93+
}
94+
95+
private static void addCassandraTableType(Object cassandraTemplate, Class<?> entityClass) {
96+
try {
97+
Object tableNameId = cassandraTemplate.getClass().getMethod("getTableName", Class.class)
98+
.invoke(cassandraTemplate, entityClass);
99+
String tableName = (String) tableNameId.getClass().getMethod("asInternal").invoke(tableNameId);
100+
101+
String schema = ClassToSchema.getOrDeriveSchemaWithItsRef(entityClass, true, Collections.emptyList());
102+
ExecutionTracer.addCassandraTableType(new CassandraTableSchema(tableName, schema));
103+
} catch (ReflectiveOperationException e) {
104+
throw new RuntimeException(e);
105+
}
106+
}
107+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package org.evomaster.client.java.instrumentation.coverage.methodreplacement.thirdpartyclasses;
2+
3+
import com.datastax.oss.driver.api.core.CqlSession;
4+
import org.evomaster.client.java.instrumentation.AdditionalInfo;
5+
import org.evomaster.client.java.instrumentation.CassandraTableSchema;
6+
import org.evomaster.client.java.instrumentation.object.ClassToSchema;
7+
import org.evomaster.client.java.instrumentation.staticstate.ExecutionTracer;
8+
import org.junit.jupiter.api.AfterAll;
9+
import org.junit.jupiter.api.BeforeAll;
10+
import org.junit.jupiter.api.BeforeEach;
11+
import org.junit.jupiter.api.Test;
12+
import org.springframework.data.cassandra.core.CassandraTemplate;
13+
import org.testcontainers.containers.GenericContainer;
14+
import org.testcontainers.containers.wait.strategy.Wait;
15+
16+
import java.net.InetSocketAddress;
17+
import java.time.Duration;
18+
import java.util.Collections;
19+
import java.util.List;
20+
import java.util.Set;
21+
import java.util.UUID;
22+
23+
import static org.junit.jupiter.api.Assertions.*;
24+
25+
public class CassandraTemplateClassReplacementTest {
26+
27+
private static CqlSession cqlSession;
28+
private static CassandraTemplate cassandraTemplate;
29+
30+
private static final int CASSANDRA_PORT = 9042;
31+
private static final String CASSANDRA_IMAGE = "cassandra";
32+
private static final String CASSANDRA_VERSION = "4.1";
33+
34+
private static final GenericContainer<?> cassandra = new GenericContainer<>(CASSANDRA_IMAGE + ":" + CASSANDRA_VERSION)
35+
.withExposedPorts(CASSANDRA_PORT)
36+
.waitingFor(Wait.forLogMessage(".*Starting listening for CQL clients.*", 1)
37+
.withStartupTimeout(Duration.ofMinutes(2)));
38+
39+
private static final String KEYSPACE = "testks";
40+
private static final String TABLE_NAME = "cassandra_template_test_dto";
41+
private static final String HOST_NAME = "localhost";
42+
private static final String DATA_CENTER = "datacenter1";
43+
44+
@BeforeAll
45+
static void startCassandra() {
46+
cassandra.start();
47+
48+
InetSocketAddress contactPoint =
49+
new InetSocketAddress(HOST_NAME, cassandra.getMappedPort(CASSANDRA_PORT));
50+
51+
// Bootstrap session, only to create the keyspace (which doesn't exist yet, so we
52+
// can't bind to it as a default keyspace until after this point)
53+
try (CqlSession bootstrap = CqlSession.builder()
54+
.addContactPoint(contactPoint)
55+
.withLocalDatacenter(DATA_CENTER)
56+
.build()) {
57+
bootstrap.execute("CREATE KEYSPACE IF NOT EXISTS " + KEYSPACE +
58+
" WITH replication = {'class':'SimpleStrategy','replication_factor':1}");
59+
}
60+
61+
// CassandraTemplate's convenience methods (insert/select/selectOneById) build CQL
62+
// using just the unqualified table name, so the session needs a default keyspace.
63+
cqlSession = CqlSession.builder()
64+
.addContactPoint(contactPoint)
65+
.withLocalDatacenter(DATA_CENTER)
66+
.withKeyspace(KEYSPACE)
67+
.build();
68+
69+
// Setup: call directly on session so it is NOT intercepted by any replacement
70+
cqlSession.execute("CREATE TABLE IF NOT EXISTS " + TABLE_NAME +
71+
" (id uuid PRIMARY KEY, name text, age int)");
72+
73+
cassandraTemplate = new CassandraTemplate(cqlSession);
74+
75+
ExecutionTracer.reset();
76+
}
77+
78+
@AfterAll
79+
static void cleanup() {
80+
if (cqlSession != null) {
81+
cqlSession.close();
82+
}
83+
ExecutionTracer.reset();
84+
}
85+
86+
@BeforeEach
87+
void clearTable() {
88+
// Direct call — not intercepted
89+
cqlSession.execute("TRUNCATE " + TABLE_NAME);
90+
ExecutionTracer.reset();
91+
}
92+
93+
private static String expectedSchema() {
94+
return ClassToSchema.getOrDeriveSchemaWithItsRef(CassandraTemplateTestDto.class, true, Collections.emptyList());
95+
}
96+
97+
private static void assertSingleRecordedTableType() {
98+
List<AdditionalInfo> additionalInfoList = ExecutionTracer.exposeAdditionalInfoList();
99+
assertEquals(1, additionalInfoList.size());
100+
101+
Set<CassandraTableSchema> tableTypes = additionalInfoList.get(0).getCassandraTableTypeData();
102+
assertEquals(1, tableTypes.size());
103+
104+
CassandraTableSchema tableSchema = tableTypes.iterator().next();
105+
assertEquals(TABLE_NAME, tableSchema.getTableName());
106+
assertEquals(expectedSchema(), tableSchema.getTableSchema());
107+
}
108+
109+
@Test
110+
void testInsert() {
111+
UUID id = UUID.randomUUID();
112+
CassandraTemplateTestDto dto = new CassandraTemplateTestDto(id, "Alice", 30);
113+
114+
CassandraTemplateClassReplacement.insert(cassandraTemplate, dto);
115+
116+
// verify the row was actually persisted, via a direct (untracked) query
117+
CassandraTemplateTestDto persisted =
118+
cassandraTemplate.selectOneById(id, CassandraTemplateTestDto.class);
119+
assertNotNull(persisted);
120+
assertEquals("Alice", persisted.name);
121+
assertEquals(30, persisted.age);
122+
123+
assertSingleRecordedTableType();
124+
}
125+
126+
@Test
127+
void testSelectOne() {
128+
UUID id = UUID.randomUUID();
129+
// Direct insert — not intercepted
130+
cqlSession.execute("INSERT INTO " + TABLE_NAME + " (id, name, age) VALUES (" + id + ", 'Bob', 25)");
131+
132+
CassandraTemplateTestDto retrieved = CassandraTemplateClassReplacement.selectOne(
133+
cassandraTemplate, "SELECT * FROM " + TABLE_NAME + " WHERE id = " + id, CassandraTemplateTestDto.class);
134+
135+
assertNotNull(retrieved);
136+
assertEquals("Bob", retrieved.name);
137+
assertEquals(25, retrieved.age);
138+
139+
assertSingleRecordedTableType();
140+
}
141+
142+
@Test
143+
void testSelect() {
144+
UUID id1 = UUID.randomUUID();
145+
UUID id2 = UUID.randomUUID();
146+
// Direct inserts — not intercepted
147+
cqlSession.execute("INSERT INTO " + TABLE_NAME + " (id, name, age) VALUES (" + id1 + ", 'Carol', 40)");
148+
cqlSession.execute("INSERT INTO " + TABLE_NAME + " (id, name, age) VALUES (" + id2 + ", 'Dave', 45)");
149+
150+
List<CassandraTemplateTestDto> retrieved = CassandraTemplateClassReplacement.select(
151+
cassandraTemplate, "SELECT * FROM " + TABLE_NAME, CassandraTemplateTestDto.class);
152+
153+
assertEquals(2, retrieved.size());
154+
155+
assertSingleRecordedTableType();
156+
}
157+
158+
@Test
159+
void testDirectCallsAreNotTracked() {
160+
UUID id = UUID.randomUUID();
161+
CassandraTemplateTestDto dto = new CassandraTemplateTestDto(id, "Erin", 22);
162+
163+
// Calls that bypass the replacement must not appear in the tracker
164+
cassandraTemplate.insert(dto);
165+
166+
List<AdditionalInfo> additionalInfoList = ExecutionTracer.exposeAdditionalInfoList();
167+
assertEquals(1, additionalInfoList.size());
168+
assertTrue(additionalInfoList.get(0).getCassandraTableTypeData().isEmpty());
169+
}
170+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.evomaster.client.java.instrumentation.coverage.methodreplacement.thirdpartyclasses;
2+
3+
import org.springframework.data.cassandra.core.mapping.PrimaryKey;
4+
import org.springframework.data.cassandra.core.mapping.Table;
5+
6+
import java.util.UUID;
7+
8+
@Table("cassandra_template_test_dto")
9+
public class CassandraTemplateTestDto {
10+
11+
@PrimaryKey
12+
public UUID id;
13+
14+
public String name;
15+
public int age;
16+
17+
public CassandraTemplateTestDto() {
18+
}
19+
20+
public CassandraTemplateTestDto(UUID id, String name, int age) {
21+
this.id = id;
22+
this.name = name;
23+
this.age = age;
24+
}
25+
}

0 commit comments

Comments
 (0)