Skip to content

Commit 0240a08

Browse files
authored
Merge pull request #1345 from WebFuzzing/feature/redis-distance-calculation
Redis handler and heuristic calculator.
2 parents 06173f6 + 6661044 commit 0240a08

File tree

16 files changed

+1473
-16
lines changed

16 files changed

+1473
-16
lines changed

client-java/controller/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,11 @@
199199
<artifactId>mongodb-driver-sync</artifactId>
200200
<scope>test</scope>
201201
</dependency>
202+
<dependency>
203+
<groupId>io.lettuce</groupId>
204+
<artifactId>lettuce-core</artifactId>
205+
<scope>test</scope>
206+
</dependency>
202207

203208
<!--gRPC RPC test-->
204209
<dependency>

client-java/controller/src/main/java/org/evomaster/client/java/controller/SutHandler.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ default void extractRPCSchema(){}
181181

182182
default Object getOpenSearchConnection() {return null;}
183183

184+
default Object getRedisConnection() {return null;}
185+
184186
/**
185187
* <p>
186188
* register or execute specified SQL script for initializing data in database
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package org.evomaster.client.java.controller.internal.db.redis;
2+
3+
import org.evomaster.client.java.instrumentation.RedisCommand;
4+
5+
/**
6+
* This class will link a given RedisCommand to the result of the distance calculation for that commmand.
7+
*/
8+
public class RedisCommandEvaluation {
9+
private final RedisCommand redisCommand;
10+
private final RedisDistanceWithMetrics redisDistanceWithMetrics;
11+
12+
public RedisCommandEvaluation(RedisCommand redisCommand, RedisDistanceWithMetrics redisDistanceWithMetrics) {
13+
this.redisCommand = redisCommand;
14+
this.redisDistanceWithMetrics = redisDistanceWithMetrics;
15+
}
16+
17+
public RedisCommand getRedisCommand() {
18+
return redisCommand;
19+
}
20+
21+
public RedisDistanceWithMetrics getRedisDistanceWithMetrics() {
22+
return redisDistanceWithMetrics;
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package org.evomaster.client.java.controller.internal.db.redis;
2+
3+
/**
4+
* This class will have the distance for a RedisCommand (between 0 and 1)
5+
* and the number of evaluated keys in that distance calculation.
6+
*/
7+
public class RedisDistanceWithMetrics {
8+
private final double redisDistance; // A number between 0 and 1.
9+
private final int numberOfEvaluatedKeys;
10+
11+
public RedisDistanceWithMetrics(double redisDistance, int numberOfEvaluatedKeys) {
12+
if(redisDistance < 0){
13+
throw new IllegalArgumentException("Distance must be non-negative but value is " + redisDistance);
14+
}
15+
if(numberOfEvaluatedKeys < 0){
16+
throw new IllegalArgumentException("Number of evaluated keys must be non-negative but value is "
17+
+ numberOfEvaluatedKeys);
18+
}
19+
this.redisDistance = redisDistance;
20+
this.numberOfEvaluatedKeys = numberOfEvaluatedKeys;
21+
}
22+
23+
public int getNumberOfEvaluatedKeys() {
24+
return numberOfEvaluatedKeys;
25+
}
26+
27+
public double getDistance() {
28+
return redisDistance;
29+
}
30+
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package org.evomaster.client.java.controller.internal.db.redis;
2+
3+
import org.evomaster.client.java.controller.internal.TaintHandlerExecutionTracer;
4+
import org.evomaster.client.java.controller.redis.RedisClient;
5+
import org.evomaster.client.java.controller.redis.RedisHeuristicsCalculator;
6+
import org.evomaster.client.java.controller.redis.RedisInfo;
7+
import org.evomaster.client.java.instrumentation.RedisCommand;
8+
import org.evomaster.client.java.utils.SimpleLogger;
9+
10+
import java.util.*;
11+
12+
import static org.evomaster.client.java.controller.redis.RedisHeuristicsCalculator.MAX_REDIS_DISTANCE;
13+
14+
/**
15+
* Class used to act upon Redis commands executed by the SUT
16+
*/
17+
public class RedisHandler {
18+
19+
/**
20+
* Info about the executed commands
21+
*/
22+
private final List<RedisCommand> operations;
23+
24+
/**
25+
* The heuristics based on the Redis execution
26+
*/
27+
private final List<RedisCommandEvaluation> evaluatedRedisCommands = new ArrayList<>();
28+
29+
/**
30+
* Whether to calculate heuristics based on execution or not
31+
*/
32+
private volatile boolean calculateHeuristics;
33+
34+
/**
35+
* Whether to use execution's info or not
36+
*/
37+
private volatile boolean extractRedisExecution;
38+
39+
/**
40+
* The client must be created through a connection factory.
41+
*/
42+
private RedisClient redisClient = null;
43+
44+
private final RedisHeuristicsCalculator calculator = new RedisHeuristicsCalculator(new TaintHandlerExecutionTracer());
45+
46+
private static final String REDIS_HASH_TYPE = "hash";
47+
private static final String REDIS_SET_TYPE = "set";
48+
private static final String REDIS_STRING_TYPE = "string";
49+
50+
public RedisHandler() {
51+
operations = new ArrayList<>();
52+
extractRedisExecution = true;
53+
calculateHeuristics = true;
54+
}
55+
56+
public void reset() {
57+
operations.clear();
58+
evaluatedRedisCommands.clear();
59+
}
60+
61+
public boolean isCalculateHeuristics() {
62+
return calculateHeuristics;
63+
}
64+
65+
public boolean isExtractRedisExecution() {
66+
return extractRedisExecution;
67+
}
68+
69+
public void setCalculateHeuristics(boolean calculateHeuristics) {
70+
this.calculateHeuristics = calculateHeuristics;
71+
}
72+
73+
public void setExtractRedisExecution(boolean extractRedisExecution) {
74+
this.extractRedisExecution = extractRedisExecution;
75+
}
76+
77+
public void handle(RedisCommand info) {
78+
if (extractRedisExecution) {
79+
operations.add(info);
80+
}
81+
}
82+
83+
public List<RedisCommandEvaluation> getEvaluatedRedisCommands() {
84+
operations.stream()
85+
.filter(command -> command.getType().shouldCalculateHeuristic())
86+
.forEach(redisCommand -> {
87+
RedisDistanceWithMetrics distanceWithMetrics = computeDistance(redisCommand, redisClient);
88+
evaluatedRedisCommands.add(new RedisCommandEvaluation(redisCommand, distanceWithMetrics));
89+
});
90+
operations.clear();
91+
92+
return evaluatedRedisCommands;
93+
}
94+
95+
private RedisDistanceWithMetrics computeDistance(RedisCommand redisCommand, RedisClient redisClient) {
96+
RedisCommand.RedisCommandType type = redisCommand.getType();
97+
try {
98+
switch (type) {
99+
case KEYS:
100+
case EXISTS: {
101+
List<RedisInfo> redisInfo = createRedisInfoForAllKeys(redisClient);
102+
return calculator.computeDistance(redisCommand, redisInfo);
103+
}
104+
105+
case GET: {
106+
List<RedisInfo> redisInfo = createRedisInfoForKeysByType(REDIS_STRING_TYPE, redisClient);
107+
return calculator.computeDistance(redisCommand, redisInfo);
108+
}
109+
110+
case HGET: {
111+
String field = redisCommand.extractArgs().get(1);
112+
List<RedisInfo> redisInfo = createRedisInfoForKeysByField(field, redisClient);
113+
return calculator.computeDistance(redisCommand, redisInfo);
114+
}
115+
116+
case HGETALL: {
117+
List<RedisInfo> redisInfo = createRedisInfoForKeysByType(REDIS_HASH_TYPE, redisClient);
118+
return calculator.computeDistance(redisCommand, redisInfo);
119+
}
120+
121+
case SMEMBERS: {
122+
List<RedisInfo> redisInfo = createRedisInfoForKeysByType(REDIS_SET_TYPE, redisClient);
123+
return calculator.computeDistance(redisCommand, redisInfo);
124+
}
125+
126+
case SINTER: {
127+
List<String> keys = redisCommand.extractArgs();
128+
List<RedisInfo> redisInfo = createRedisInfoForIntersection(keys, redisClient);
129+
return calculator.computeDistance(redisCommand, redisInfo);
130+
}
131+
132+
default:
133+
return new RedisDistanceWithMetrics(MAX_REDIS_DISTANCE, 0);
134+
}
135+
} catch (Exception e) {
136+
SimpleLogger.warn("Could not compute distance for " + type + ": " + e.getMessage());
137+
return new RedisDistanceWithMetrics(MAX_REDIS_DISTANCE, 0);
138+
}
139+
}
140+
141+
private List<RedisInfo> createRedisInfoForIntersection(List<String> keys, RedisClient redisClient) {
142+
List<RedisInfo> redisData = new ArrayList<>();
143+
keys.forEach(
144+
key -> redisData.add(new RedisInfo(key, redisClient.getType(key), redisClient.getSetMembers(key))
145+
));
146+
return redisData;
147+
}
148+
149+
private List<RedisInfo> createRedisInfoForAllKeys(RedisClient redisClient) {
150+
Set<String> keys = redisClient.getAllKeys();
151+
List<RedisInfo> redisData = new ArrayList<>();
152+
keys.forEach(
153+
key -> redisData.add(new RedisInfo(key))
154+
);
155+
return redisData;
156+
}
157+
158+
private List<RedisInfo> createRedisInfoForKeysByType(String type, RedisClient redisClient) {
159+
Set<String> keys = redisClient.getKeysByType(type);
160+
List<RedisInfo> redisData = new ArrayList<>();
161+
keys.forEach(key -> redisData.add(new RedisInfo(key)));
162+
return redisData;
163+
}
164+
165+
private List<RedisInfo> createRedisInfoForKeysByField(String field, RedisClient redisClient) {
166+
Set<String> keys = redisClient.getKeysByType(REDIS_HASH_TYPE);
167+
List<RedisInfo> redisData = new ArrayList<>();
168+
keys.forEach(key -> redisData.add(new RedisInfo(key, redisClient.hashFieldExists(key, field))));
169+
return redisData;
170+
}
171+
172+
public void setRedisClient(RedisClient redisClient) {
173+
this.redisClient = redisClient;
174+
}
175+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package org.evomaster.client.java.controller.redis;
2+
3+
import java.lang.reflect.Method;
4+
import java.util.*;
5+
import java.util.stream.Collectors;
6+
7+
/**
8+
* RedisClient that uses Lettuce dynamically via reflection, avoiding
9+
* compile-time dependency on Spring or Lettuce.
10+
*/
11+
public class RedisClient {
12+
13+
private final Object redisClient; // io.lettuce.core.RedisClient
14+
private final Object connection; // io.lettuce.core.api.StatefulRedisConnection
15+
private final Object syncCommands; // io.lettuce.core.api.sync.RedisCommands
16+
17+
public RedisClient(String host, int port) {
18+
try {
19+
Class<?> redisClientClass = Class.forName("io.lettuce.core.RedisClient");
20+
Class<?> redisURIClass = Class.forName("io.lettuce.core.RedisURI");
21+
22+
Method createUri = redisURIClass.getMethod("create", String.class);
23+
Object uri = createUri.invoke(null, "redis://" + host + ":" + port);
24+
25+
Method createClient = redisClientClass.getMethod("create", redisURIClass);
26+
this.redisClient = createClient.invoke(null, uri);
27+
28+
Method connectMethod = redisClientClass.getMethod("connect");
29+
this.connection = connectMethod.invoke(redisClient);
30+
31+
Class<?> statefulConnClass = Class.forName("io.lettuce.core.api.StatefulRedisConnection");
32+
Method syncMethod = statefulConnClass.getMethod("sync");
33+
this.syncCommands = syncMethod.invoke(connection);
34+
35+
} catch (Exception e) {
36+
throw new RuntimeException("Failed to initialize Lettuce Redis client via reflection", e);
37+
}
38+
}
39+
40+
public void close() {
41+
try {
42+
if (connection != null) {
43+
Method close = connection.getClass().getMethod("close");
44+
close.invoke(connection);
45+
}
46+
if (redisClient != null) {
47+
Method shutdown = redisClient.getClass().getMethod("shutdown");
48+
shutdown.invoke(redisClient);
49+
}
50+
} catch (Exception ignored) {}
51+
}
52+
53+
/** Equivalent to SET key value */
54+
public void setValue(String key, String value) {
55+
invoke("set", key, value);
56+
}
57+
58+
/** Equivalent to GET key */
59+
public String getValue(String key) {
60+
return (String) invoke("get", key);
61+
}
62+
63+
/** Equivalent to KEYS * */
64+
public Set<String> getAllKeys() {
65+
Object result = invoke("keys", "*");
66+
if (result instanceof Collection)
67+
return new HashSet<>((Collection<String>) result);
68+
return Collections.emptySet();
69+
}
70+
71+
/** Equivalent to TYPE key */
72+
public String getType(String key) {
73+
Object result = invoke("type", key);
74+
return result != null ? result.toString() : null;
75+
}
76+
77+
/** HSET key field value */
78+
public void hashSet(String key, String field, String value) {
79+
invoke("hset", key, field, value);
80+
}
81+
82+
/** HEXISTS key field */
83+
public boolean hashFieldExists(String key, String field) {
84+
Object result = invoke("hexists", key, field);
85+
return result instanceof Boolean && (Boolean) result;
86+
}
87+
88+
/** SMEMBERS key */
89+
public Set<String> getSetMembers(String key) {
90+
Object result = invoke("smembers", key);
91+
if (result instanceof Collection)
92+
return new HashSet<>((Collection<String>) result);
93+
return Collections.emptySet();
94+
}
95+
96+
private Object invoke(String methodName, Object... args) {
97+
try {
98+
Class<?>[] argTypes = Arrays.stream(args)
99+
.map(Object::getClass)
100+
.toArray(Class<?>[]::new);
101+
102+
Method method = findMethod(syncCommands.getClass(), methodName, argTypes);
103+
if (method == null)
104+
throw new RuntimeException("Method not found: " + methodName);
105+
return method.invoke(syncCommands, args);
106+
107+
} catch (Exception e) {
108+
throw new RuntimeException("Error invoking Redis command: " + methodName, e);
109+
}
110+
}
111+
112+
private Method findMethod(Class<?> clazz, String name, Class<?>[] argTypes) {
113+
for (Method m : clazz.getMethods()) {
114+
if (!m.getName().equals(name)) continue;
115+
if (m.getParameterCount() != argTypes.length) continue;
116+
return m;
117+
}
118+
return null;
119+
}
120+
121+
public Set<String> getKeysByType(String expectedType) {
122+
return getAllKeys().stream()
123+
.filter(k -> expectedType.equalsIgnoreCase(getType(k)))
124+
.collect(Collectors.toSet());
125+
}
126+
127+
public void flushAll() {
128+
invoke("flushall");
129+
}
130+
}

0 commit comments

Comments
 (0)