diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/SutHandler.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/SutHandler.java
index 0a60013794..5ee0cc89df 100644
--- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/SutHandler.java
+++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/SutHandler.java
@@ -181,6 +181,8 @@ default void extractRPCSchema(){}
default Object getOpenSearchConnection() {return null;}
+ default Object getRedisConnection() {return null;}
+
/**
*
* register or execute specified SQL script for initializing data in database
diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/db/redis/RedisCommandEvaluation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/db/redis/RedisCommandEvaluation.java
new file mode 100644
index 0000000000..702314a1bb
--- /dev/null
+++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/db/redis/RedisCommandEvaluation.java
@@ -0,0 +1,24 @@
+package org.evomaster.client.java.controller.internal.db.redis;
+
+import org.evomaster.client.java.instrumentation.RedisCommand;
+
+/**
+ * This class will link a given RedisCommand to the result of the distance calculation for that commmand.
+ */
+public class RedisCommandEvaluation {
+ private final RedisCommand redisCommand;
+ private final RedisDistanceWithMetrics redisDistanceWithMetrics;
+
+ public RedisCommandEvaluation(RedisCommand redisCommand, RedisDistanceWithMetrics redisDistanceWithMetrics) {
+ this.redisCommand = redisCommand;
+ this.redisDistanceWithMetrics = redisDistanceWithMetrics;
+ }
+
+ public RedisCommand getRedisCommand() {
+ return redisCommand;
+ }
+
+ public RedisDistanceWithMetrics getRedisDistanceWithMetrics() {
+ return redisDistanceWithMetrics;
+ }
+}
diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/db/redis/RedisDistanceWithMetrics.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/db/redis/RedisDistanceWithMetrics.java
new file mode 100644
index 0000000000..8a684d29ab
--- /dev/null
+++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/db/redis/RedisDistanceWithMetrics.java
@@ -0,0 +1,30 @@
+package org.evomaster.client.java.controller.internal.db.redis;
+
+/**
+ * This class will have the distance for a RedisCommand (between 0 and 1)
+ * and the number of evaluated keys in that distance calculation.
+ */
+public class RedisDistanceWithMetrics {
+ private final double redisDistance; // A number between 0 and 1.
+ private final int numberOfEvaluatedKeys;
+
+ public RedisDistanceWithMetrics(double redisDistance, int numberOfEvaluatedKeys) {
+ if(redisDistance < 0){
+ throw new IllegalArgumentException("Distance must be non-negative but value is " + redisDistance);
+ }
+ if(numberOfEvaluatedKeys < 0){
+ throw new IllegalArgumentException("Number of evaluated keys must be non-negative but value is "
+ + numberOfEvaluatedKeys);
+ }
+ this.redisDistance = redisDistance;
+ this.numberOfEvaluatedKeys = numberOfEvaluatedKeys;
+ }
+
+ public int getNumberOfEvaluatedKeys() {
+ return numberOfEvaluatedKeys;
+ }
+
+ public double getDistance() {
+ return redisDistance;
+ }
+}
diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/db/redis/RedisHandler.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/db/redis/RedisHandler.java
new file mode 100644
index 0000000000..bc737713a7
--- /dev/null
+++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/db/redis/RedisHandler.java
@@ -0,0 +1,175 @@
+package org.evomaster.client.java.controller.internal.db.redis;
+
+import org.evomaster.client.java.controller.internal.TaintHandlerExecutionTracer;
+import org.evomaster.client.java.controller.redis.RedisClient;
+import org.evomaster.client.java.controller.redis.RedisHeuristicsCalculator;
+import org.evomaster.client.java.controller.redis.RedisInfo;
+import org.evomaster.client.java.instrumentation.RedisCommand;
+import org.evomaster.client.java.utils.SimpleLogger;
+
+import java.util.*;
+
+import static org.evomaster.client.java.controller.redis.RedisHeuristicsCalculator.MAX_REDIS_DISTANCE;
+
+/**
+ * Class used to act upon Redis commands executed by the SUT
+ */
+public class RedisHandler {
+
+ /**
+ * Info about the executed commands
+ */
+ private final List operations;
+
+ /**
+ * The heuristics based on the Redis execution
+ */
+ private final List evaluatedRedisCommands = new ArrayList<>();
+
+ /**
+ * Whether to calculate heuristics based on execution or not
+ */
+ private volatile boolean calculateHeuristics;
+
+ /**
+ * Whether to use execution's info or not
+ */
+ private volatile boolean extractRedisExecution;
+
+ /**
+ * The client must be created through a connection factory.
+ */
+ private RedisClient redisClient = null;
+
+ private final RedisHeuristicsCalculator calculator = new RedisHeuristicsCalculator(new TaintHandlerExecutionTracer());
+
+ private static final String REDIS_HASH_TYPE = "hash";
+ private static final String REDIS_SET_TYPE = "set";
+ private static final String REDIS_STRING_TYPE = "string";
+
+ public RedisHandler() {
+ operations = new ArrayList<>();
+ extractRedisExecution = true;
+ calculateHeuristics = true;
+ }
+
+ public void reset() {
+ operations.clear();
+ evaluatedRedisCommands.clear();
+ }
+
+ public boolean isCalculateHeuristics() {
+ return calculateHeuristics;
+ }
+
+ public boolean isExtractRedisExecution() {
+ return extractRedisExecution;
+ }
+
+ public void setCalculateHeuristics(boolean calculateHeuristics) {
+ this.calculateHeuristics = calculateHeuristics;
+ }
+
+ public void setExtractRedisExecution(boolean extractRedisExecution) {
+ this.extractRedisExecution = extractRedisExecution;
+ }
+
+ public void handle(RedisCommand info) {
+ if (extractRedisExecution) {
+ operations.add(info);
+ }
+ }
+
+ public List getEvaluatedRedisCommands() {
+ operations.stream()
+ .filter(command -> command.getType().shouldCalculateHeuristic())
+ .forEach(redisCommand -> {
+ RedisDistanceWithMetrics distanceWithMetrics = computeDistance(redisCommand, redisClient);
+ evaluatedRedisCommands.add(new RedisCommandEvaluation(redisCommand, distanceWithMetrics));
+ });
+ operations.clear();
+
+ return evaluatedRedisCommands;
+ }
+
+ private RedisDistanceWithMetrics computeDistance(RedisCommand redisCommand, RedisClient redisClient) {
+ RedisCommand.RedisCommandType type = redisCommand.getType();
+ try {
+ switch (type) {
+ case KEYS:
+ case EXISTS: {
+ List redisInfo = createRedisInfoForAllKeys(redisClient);
+ return calculator.computeDistance(redisCommand, redisInfo);
+ }
+
+ case GET: {
+ List redisInfo = createRedisInfoForKeysByType(REDIS_STRING_TYPE, redisClient);
+ return calculator.computeDistance(redisCommand, redisInfo);
+ }
+
+ case HGET: {
+ String field = redisCommand.extractArgs().get(1);
+ List redisInfo = createRedisInfoForKeysByField(field, redisClient);
+ return calculator.computeDistance(redisCommand, redisInfo);
+ }
+
+ case HGETALL: {
+ List redisInfo = createRedisInfoForKeysByType(REDIS_HASH_TYPE, redisClient);
+ return calculator.computeDistance(redisCommand, redisInfo);
+ }
+
+ case SMEMBERS: {
+ List redisInfo = createRedisInfoForKeysByType(REDIS_SET_TYPE, redisClient);
+ return calculator.computeDistance(redisCommand, redisInfo);
+ }
+
+ case SINTER: {
+ List keys = redisCommand.extractArgs();
+ List redisInfo = createRedisInfoForIntersection(keys, redisClient);
+ return calculator.computeDistance(redisCommand, redisInfo);
+ }
+
+ default:
+ return new RedisDistanceWithMetrics(MAX_REDIS_DISTANCE, 0);
+ }
+ } catch (Exception e) {
+ SimpleLogger.warn("Could not compute distance for " + type + ": " + e.getMessage());
+ return new RedisDistanceWithMetrics(MAX_REDIS_DISTANCE, 0);
+ }
+ }
+
+ private List createRedisInfoForIntersection(List keys, RedisClient redisClient) {
+ List redisData = new ArrayList<>();
+ keys.forEach(
+ key -> redisData.add(new RedisInfo(key, redisClient.getType(key), redisClient.getSetMembers(key))
+ ));
+ return redisData;
+ }
+
+ private List createRedisInfoForAllKeys(RedisClient redisClient) {
+ Set keys = redisClient.getAllKeys();
+ List redisData = new ArrayList<>();
+ keys.forEach(
+ key -> redisData.add(new RedisInfo(key))
+ );
+ return redisData;
+ }
+
+ private List createRedisInfoForKeysByType(String type, RedisClient redisClient) {
+ Set keys = redisClient.getKeysByType(type);
+ List redisData = new ArrayList<>();
+ keys.forEach(key -> redisData.add(new RedisInfo(key)));
+ return redisData;
+ }
+
+ private List createRedisInfoForKeysByField(String field, RedisClient redisClient) {
+ Set keys = redisClient.getKeysByType(REDIS_HASH_TYPE);
+ List redisData = new ArrayList<>();
+ keys.forEach(key -> redisData.add(new RedisInfo(key, redisClient.hashFieldExists(key, field))));
+ return redisData;
+ }
+
+ public void setRedisClient(RedisClient redisClient) {
+ this.redisClient = redisClient;
+ }
+}
diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/RedisClient.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/RedisClient.java
new file mode 100644
index 0000000000..2969e17b46
--- /dev/null
+++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/RedisClient.java
@@ -0,0 +1,130 @@
+package org.evomaster.client.java.controller.redis;
+
+import java.lang.reflect.Method;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * RedisClient that uses Lettuce dynamically via reflection, avoiding
+ * compile-time dependency on Spring or Lettuce.
+ */
+public class RedisClient {
+
+ private final Object redisClient; // io.lettuce.core.RedisClient
+ private final Object connection; // io.lettuce.core.api.StatefulRedisConnection
+ private final Object syncCommands; // io.lettuce.core.api.sync.RedisCommands
+
+ public RedisClient(String host, int port) {
+ try {
+ Class> redisClientClass = Class.forName("io.lettuce.core.RedisClient");
+ Class> redisURIClass = Class.forName("io.lettuce.core.RedisURI");
+
+ Method createUri = redisURIClass.getMethod("create", String.class);
+ Object uri = createUri.invoke(null, "redis://" + host + ":" + port);
+
+ Method createClient = redisClientClass.getMethod("create", redisURIClass);
+ this.redisClient = createClient.invoke(null, uri);
+
+ Method connectMethod = redisClientClass.getMethod("connect");
+ this.connection = connectMethod.invoke(redisClient);
+
+ Class> statefulConnClass = Class.forName("io.lettuce.core.api.StatefulRedisConnection");
+ Method syncMethod = statefulConnClass.getMethod("sync");
+ this.syncCommands = syncMethod.invoke(connection);
+
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to initialize Lettuce Redis client via reflection", e);
+ }
+ }
+
+ public void close() {
+ try {
+ if (connection != null) {
+ Method close = connection.getClass().getMethod("close");
+ close.invoke(connection);
+ }
+ if (redisClient != null) {
+ Method shutdown = redisClient.getClass().getMethod("shutdown");
+ shutdown.invoke(redisClient);
+ }
+ } catch (Exception ignored) {}
+ }
+
+ /** Equivalent to SET key value */
+ public void setValue(String key, String value) {
+ invoke("set", key, value);
+ }
+
+ /** Equivalent to GET key */
+ public String getValue(String key) {
+ return (String) invoke("get", key);
+ }
+
+ /** Equivalent to KEYS * */
+ public Set getAllKeys() {
+ Object result = invoke("keys", "*");
+ if (result instanceof Collection)
+ return new HashSet<>((Collection) result);
+ return Collections.emptySet();
+ }
+
+ /** Equivalent to TYPE key */
+ public String getType(String key) {
+ Object result = invoke("type", key);
+ return result != null ? result.toString() : null;
+ }
+
+ /** HSET key field value */
+ public void hashSet(String key, String field, String value) {
+ invoke("hset", key, field, value);
+ }
+
+ /** HEXISTS key field */
+ public boolean hashFieldExists(String key, String field) {
+ Object result = invoke("hexists", key, field);
+ return result instanceof Boolean && (Boolean) result;
+ }
+
+ /** SMEMBERS key */
+ public Set getSetMembers(String key) {
+ Object result = invoke("smembers", key);
+ if (result instanceof Collection)
+ return new HashSet<>((Collection) result);
+ return Collections.emptySet();
+ }
+
+ private Object invoke(String methodName, Object... args) {
+ try {
+ Class>[] argTypes = Arrays.stream(args)
+ .map(Object::getClass)
+ .toArray(Class>[]::new);
+
+ Method method = findMethod(syncCommands.getClass(), methodName, argTypes);
+ if (method == null)
+ throw new RuntimeException("Method not found: " + methodName);
+ return method.invoke(syncCommands, args);
+
+ } catch (Exception e) {
+ throw new RuntimeException("Error invoking Redis command: " + methodName, e);
+ }
+ }
+
+ private Method findMethod(Class> clazz, String name, Class>[] argTypes) {
+ for (Method m : clazz.getMethods()) {
+ if (!m.getName().equals(name)) continue;
+ if (m.getParameterCount() != argTypes.length) continue;
+ return m;
+ }
+ return null;
+ }
+
+ public Set getKeysByType(String expectedType) {
+ return getAllKeys().stream()
+ .filter(k -> expectedType.equalsIgnoreCase(getType(k)))
+ .collect(Collectors.toSet());
+ }
+
+ public void flushAll() {
+ invoke("flushall");
+ }
+}
\ No newline at end of file
diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/RedisHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/RedisHeuristicsCalculator.java
new file mode 100644
index 0000000000..79085258cd
--- /dev/null
+++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/RedisHeuristicsCalculator.java
@@ -0,0 +1,248 @@
+package org.evomaster.client.java.controller.redis;
+
+import org.evomaster.client.java.controller.internal.db.redis.RedisDistanceWithMetrics;
+import org.evomaster.client.java.distance.heuristics.DistanceHelper;
+import org.evomaster.client.java.distance.heuristics.TruthnessUtils;
+import org.evomaster.client.java.instrumentation.RedisCommand;
+import org.evomaster.client.java.instrumentation.coverage.methodreplacement.RegexDistanceUtils;
+import org.evomaster.client.java.sql.internal.TaintHandler;
+import org.evomaster.client.java.utils.SimpleLogger;
+
+import java.util.*;
+
+import static org.evomaster.client.java.controller.redis.RedisUtils.redisPatternToRegex;
+
+public class RedisHeuristicsCalculator {
+
+ public static final double MAX_REDIS_DISTANCE = 1d;
+
+ private final TaintHandler taintHandler;
+
+ public RedisHeuristicsCalculator() {
+ this(null);
+ }
+
+ public RedisHeuristicsCalculator(TaintHandler taintHandler) {
+ this.taintHandler = taintHandler;
+ }
+
+ /**
+ * Computes the distance of a given redis command in Redis.
+ * Dispatches the computation based on the command keyword (type).
+ *
+ * @param redisCommand Redis command.
+ * @param redisInfo Redis data in a generic structure.
+ * @return RedisDistanceWithMetrics
+ */
+ public RedisDistanceWithMetrics computeDistance(RedisCommand redisCommand, List redisInfo) {
+ RedisCommand.RedisCommandType type = redisCommand.getType();
+ try {
+ switch (type) {
+ case KEYS: {
+ String pattern = redisCommand.extractArgs().get(0);
+ return calculateDistanceForPattern(pattern, redisInfo);
+ }
+
+ case EXISTS:
+ case GET:
+ case HGETALL:
+ case SMEMBERS: {
+ String target = redisCommand.extractArgs().get(0);
+ return calculateDistanceForKeyMatch(target, redisInfo);
+ }
+
+ case HGET: {
+ String key = redisCommand.extractArgs().get(0);
+ return calculateDistanceForFieldInHash(key, redisInfo);
+ }
+
+ case SINTER: {
+ return calculateDistanceForIntersection(redisInfo);
+ }
+
+ default:
+ return new RedisDistanceWithMetrics(MAX_REDIS_DISTANCE, 0);
+ }
+ } catch (Exception e) {
+ SimpleLogger.warn("Could not compute distance for " + type + ": " + e.getMessage());
+ return new RedisDistanceWithMetrics(MAX_REDIS_DISTANCE, 0);
+ }
+ }
+
+ /**
+ * Computes the distance of a given pattern to the keys in Redis.
+ *
+ * @param pattern Pattern used to retrieve keys.
+ * @param keys List of keys.
+ * @return RedisDistanceWithMetrics
+ */
+ private RedisDistanceWithMetrics calculateDistanceForPattern(
+ String pattern,
+ List keys) {
+ double minDist = MAX_REDIS_DISTANCE;
+ int eval = 0;
+ String regex;
+ try {
+ regex = redisPatternToRegex(pattern);
+ } catch (IllegalArgumentException e) {
+ SimpleLogger.uniqueWarn("Invalid Redis pattern. Cannot compute regex for: " + pattern);
+ return new RedisDistanceWithMetrics(MAX_REDIS_DISTANCE, 0);
+ }
+ for (RedisInfo k : keys) {
+ double d = TruthnessUtils.normalizeValue(
+ RegexDistanceUtils.getStandardDistance(k.getKey(), redisPatternToRegex(pattern)));
+ minDist = Math.min(minDist, d);
+ eval++;
+ if (d == 0) return new RedisDistanceWithMetrics(0, eval);
+ }
+ return new RedisDistanceWithMetrics(minDist, eval);
+ }
+
+ /**
+ * Computes the distance of a given command (currently EXISTS, GET, HGETALL, SMEMBERS)
+ * using the target key against the candidate keys.
+ *
+ * @param targetKey Primary key used in the command.
+ * @param candidateKeys Keys from Redis of the same type as the command expects.
+ * @return RedisDistanceWithMetrics
+ */
+ private RedisDistanceWithMetrics calculateDistanceForKeyMatch(
+ String targetKey,
+ List candidateKeys
+ ) {
+ if (candidateKeys.isEmpty()) {
+ return new RedisDistanceWithMetrics(MAX_REDIS_DISTANCE, 0);
+ }
+
+ double minDist = MAX_REDIS_DISTANCE;
+ int evaluated = 0;
+
+ for (RedisInfo k : candidateKeys) {
+ try {
+ long rawDist = DistanceHelper.getLeftAlignmentDistance(targetKey, k.getKey());
+ double normDist = TruthnessUtils.normalizeValue(rawDist);
+ minDist = Math.min(minDist, normDist);
+ evaluated++;
+
+ if (normDist == 0) {
+ return new RedisDistanceWithMetrics(0, evaluated);
+ }
+ } catch (Exception ex) {
+ SimpleLogger.uniqueWarn("Failed to compute distance for key " + k + ": " + ex.getMessage());
+ }
+ }
+
+ return new RedisDistanceWithMetrics(minDist, evaluated);
+ }
+
+ /**
+ * Computes the distance of a given hash commend (HGET) considering both the key and the hash field.
+ *
+ * @param targetKey Primary key used in the command
+ * @param keys Redis data
+ * @return RedisDistanceWithMetrics
+ */
+ private RedisDistanceWithMetrics calculateDistanceForFieldInHash(
+ String targetKey,
+ List keys
+ ) {
+ if (keys.isEmpty()) {
+ return new RedisDistanceWithMetrics(MAX_REDIS_DISTANCE, 0);
+ }
+
+ double minDist = MAX_REDIS_DISTANCE;
+ int evaluated = 0;
+
+ for (RedisInfo k : keys) {
+ try {
+ long keyDist = DistanceHelper.getLeftAlignmentDistance(targetKey, k.getKey());
+
+ double fieldDist = k.hasField() ? 0d : MAX_REDIS_DISTANCE;
+
+ double combined = TruthnessUtils.normalizeValue(keyDist + fieldDist);
+
+ minDist = Math.min(minDist, combined);
+ evaluated++;
+
+ if (combined == 0) {
+ return new RedisDistanceWithMetrics(0, evaluated);
+ }
+ } catch (Exception ex) {
+ SimpleLogger.uniqueWarn("Failed HGET distance on " + k + ": " + ex.getMessage());
+ }
+ }
+
+ return new RedisDistanceWithMetrics(minDist, evaluated);
+ }
+
+ /**
+ * Computes the distance of a given intersection considering the keys for the given sets.
+ *
+ * @param keys Set keys for the intersection
+ * @return RedisDistanceWithMetrics
+ */
+ private RedisDistanceWithMetrics calculateDistanceForIntersection(
+ List keys
+ ) {
+ if (keys == null || keys.isEmpty()) {
+ return new RedisDistanceWithMetrics(MAX_REDIS_DISTANCE, 0);
+ }
+
+ double total = 0d;
+ int evaluated = 0;
+
+ Set currentIntersection = null;
+
+ for (int i = 0; i < keys.size(); i++) {
+ RedisInfo k = keys.get(i);
+ String type = k.getType();
+ if (!"set".equalsIgnoreCase(type)) {
+ return new RedisDistanceWithMetrics(MAX_REDIS_DISTANCE, evaluated);
+ }
+
+ Set set = k.getMembers();
+ if (set == null) set = Collections.emptySet();
+
+ if (i == 0) {
+ currentIntersection = new HashSet<>(set);
+ double d0 = currentIntersection.isEmpty() ? MAX_REDIS_DISTANCE : 0d;
+ total += d0;
+ evaluated++;
+ } else {
+ Set newIntersection = new HashSet<>(currentIntersection);
+ newIntersection.retainAll(set);
+
+ double di = newIntersection.isEmpty()
+ ? computeSetIntersectionDistance(currentIntersection, set)
+ : 0d;
+
+ total += di;
+ evaluated++;
+ currentIntersection = newIntersection;
+ }
+ }
+
+ return new RedisDistanceWithMetrics(total / keys.size(), evaluated);
+ }
+
+ /**
+ * Intersection distance between two given sets.
+ */
+ private double computeSetIntersectionDistance(Set s1, Set s2) {
+ if (s1.isEmpty() || s2.isEmpty()) {
+ return MAX_REDIS_DISTANCE;
+ }
+
+ double min = MAX_REDIS_DISTANCE;
+ for (String a : s1) {
+ for (String b : s2) {
+ long raw = DistanceHelper.getLeftAlignmentDistance(a, b);
+ double norm = TruthnessUtils.normalizeValue(raw);
+ min = Math.min(min, norm);
+ if (min == 0) return 0;
+ }
+ }
+ return min;
+ }
+
+}
diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/RedisInfo.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/RedisInfo.java
new file mode 100644
index 0000000000..51c7fa2a41
--- /dev/null
+++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/RedisInfo.java
@@ -0,0 +1,46 @@
+package org.evomaster.client.java.controller.redis;
+
+import java.util.Set;
+
+/**
+ * This class will contain all necessary information from Redis to perform the distance calculation for a given command.
+ * Hence, RedisHeuristicCalculator will be decoupled from Redis.
+ * There'll be no need to call Redis to calculate distances.
+ */
+public class RedisInfo {
+ private String key;
+ private String type;
+ private boolean hasField;
+ private Set members;
+
+ public RedisInfo(String key) {
+ this.key = key;
+ }
+
+ public RedisInfo(String key, boolean hasField) {
+ this.key = key;
+ this.hasField = hasField;
+ }
+
+ public RedisInfo(String key, String type, Set members) {
+ this.key = key;
+ this.type = type;
+ this.members = members;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public boolean hasField() {
+ return hasField;
+ }
+
+ public Set getMembers() {
+ return members;
+ }
+}
diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/RedisUtils.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/RedisUtils.java
new file mode 100644
index 0000000000..f2be135961
--- /dev/null
+++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/RedisUtils.java
@@ -0,0 +1,104 @@
+package org.evomaster.client.java.controller.redis;
+
+/**
+ * Utils class for auxiliary operations in Redis heuristic value calculations.
+ */
+public class RedisUtils {
+
+ /**
+ * Translates a Redis glob-style pattern into a valid Java regex pattern.
+ * Supported glob-style patterns:
+ *
+ * h?llo matches hello, hallo and hxllo
+ * h*llo matches hllo and heeeello
+ * h[ae]llo matches hello and hallo, but not hillo
+ * h[^e]llo matches hallo, hbllo, ... but not hello
+ * h[a-b]llo matches hallo and hbllo
+ * Use \ to escape special characters if you want to match them verbatim.
+ *
+ * Supported conversions:
+ * - * → .*
+ * - ? → .
+ * - [ae] → [ae]
+ * - [^e] → [^e]
+ * - [a-b] → [a-b]
+ * Other regex metacharacters are properly escaped.
+ *
+ * @param redisPattern the Redis glob-style pattern (e.g., "h?llo*", "user:[0-9]*")
+ * @return a valid Java regex string equivalent to the Redis pattern.
+ */
+ public static String redisPatternToRegex(String redisPattern) {
+ if (redisPattern == null || redisPattern.isEmpty()) {
+ return ".*";
+ }
+
+ StringBuilder regex = new StringBuilder();
+ boolean inBrackets = false;
+ boolean escaping = false;
+
+ for (int i = 0; i < redisPattern.length(); i++) {
+ char c = redisPattern.charAt(i);
+
+ if (escaping) {
+ if (".+(){}|^$[]\\".indexOf(c) >= 0) {
+ regex.append('\\');
+ }
+ regex.append(c);
+ escaping = false;
+ continue;
+ }
+
+ switch (c) {
+ case '\\':
+ escaping = true;
+ break;
+
+ case '*':
+ regex.append(".*");
+ break;
+
+ case '?':
+ regex.append('.');
+ break;
+
+ case '[':
+ if (inBrackets) {
+ throw new IllegalArgumentException("Malformed Redis pattern: nested [");
+ }
+ inBrackets = true;
+ regex.append('[');
+ if (i + 1 < redisPattern.length() && redisPattern.charAt(i + 1) == '^') {
+ regex.append('^');
+ i++;
+ }
+ break;
+
+ case ']':
+ if (inBrackets) {
+ regex.append(']');
+ inBrackets = false;
+ } else {
+ regex.append("\\]");
+ }
+ break;
+
+ default:
+ if (!inBrackets && ".+(){}|^$".indexOf(c) >= 0) {
+ regex.append('\\');
+ }
+ regex.append(c);
+ break;
+ }
+ }
+
+ if (escaping) {
+ throw new IllegalArgumentException("Malformed Redis pattern: trailing backslash");
+ }
+
+ if (inBrackets) {
+ throw new IllegalArgumentException("Malformed Redis pattern: unclosed [");
+ }
+
+ return "^" + regex + "$";
+ }
+}
diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/RedisDistanceWithMetricsTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/RedisDistanceWithMetricsTest.java
new file mode 100644
index 0000000000..ad34465eaa
--- /dev/null
+++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/RedisDistanceWithMetricsTest.java
@@ -0,0 +1,24 @@
+package org.evomaster.client.java.controller.internal.db;
+
+import org.evomaster.client.java.controller.internal.db.redis.RedisDistanceWithMetrics;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class RedisDistanceWithMetricsTest {
+
+ @Test
+ public void testNegativeRedisDistance() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new RedisDistanceWithMetrics(-1.0, 0)
+ );
+ }
+
+ @Test
+ public void testNegativeNumberOfDocuments() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new RedisDistanceWithMetrics(1.0, -1)
+ );
+ }
+
+}
diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/redis/RedisHandlerIntegrationTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/redis/RedisHandlerIntegrationTest.java
new file mode 100644
index 0000000000..3bf0baa234
--- /dev/null
+++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/redis/RedisHandlerIntegrationTest.java
@@ -0,0 +1,107 @@
+package org.evomaster.client.java.controller.internal.db.redis;
+
+import org.evomaster.client.java.instrumentation.RedisCommand;
+import org.evomaster.client.java.controller.redis.RedisClient;
+import org.junit.jupiter.api.*;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.utility.DockerImageName;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class RedisHandlerIntegrationTest {
+
+ private static final int REDIS_PORT = 6379;
+ private GenericContainer> redisContainer;
+ private RedisClient client;
+ private RedisHandler handler;
+ private int port;
+
+ @BeforeAll
+ void setupContainer() {
+ redisContainer = new GenericContainer<>(DockerImageName.parse("redis:7.0"))
+ .withExposedPorts(REDIS_PORT);
+ redisContainer.start();
+
+ port = redisContainer.getMappedPort(REDIS_PORT);
+
+ client = new RedisClient("localhost", port);
+ }
+
+ @BeforeEach
+ void setupHandler() {
+ handler = new RedisHandler();
+ handler.setRedisClient(client);
+ handler.setCalculateHeuristics(true);
+ handler.setExtractRedisExecution(true);
+ client.flushAll();
+ }
+
+ @AfterAll
+ void teardown() {
+ redisContainer.stop();
+ }
+
+ @Test
+ void testHeuristicDistanceForStringExists() {
+ client.setValue("user:1", "John");
+ client.setValue("user:2", "Jane");
+ assertEquals(2, client.getAllKeys().size());
+
+ RedisCommand similarKeyCmd = new RedisCommand(
+ RedisCommand.RedisCommandType.EXISTS,
+ new String[]{"key"},
+ true,
+ 10
+ );
+ RedisCommand differentKeyCmd = new RedisCommand(
+ RedisCommand.RedisCommandType.EXISTS,
+ new String[]{"key"},
+ true,
+ 10
+ );
+
+ handler.handle(similarKeyCmd);
+ handler.handle(differentKeyCmd);
+
+ List evals = handler.getEvaluatedRedisCommands();
+ assertEquals(2, evals.size(), "Should be two command evaluations.");
+
+ RedisCommandEvaluation evalForSimilar = evals.get(0);
+ assertNotNull(evalForSimilar.getRedisDistanceWithMetrics());
+ RedisCommandEvaluation evalForDifferent = evals.get(1);
+ assertNotNull(evalForDifferent.getRedisDistanceWithMetrics());
+
+ double distanceForSimilar = evalForSimilar.getRedisDistanceWithMetrics().getDistance();
+ int evaluatedForSimilar = evalForSimilar.getRedisDistanceWithMetrics().getNumberOfEvaluatedKeys();
+
+ assertTrue(distanceForSimilar >= 0 && distanceForSimilar <= 1,
+ "Distance should be between 0 and 1");
+ assertEquals(2, evaluatedForSimilar, "Both keys should be evaluated.");
+
+ double distanceForDifferent = evalForDifferent.getRedisDistanceWithMetrics().getDistance();
+ int evaluatedForDifferent = evalForDifferent.getRedisDistanceWithMetrics().getNumberOfEvaluatedKeys();
+
+ assertTrue(distanceForDifferent >= 0 && distanceForDifferent <= 1,
+ "Distance should be between 0 and 1");
+ assertEquals(2, evaluatedForDifferent, "Both keys should be evaluated.");
+ assertTrue(distanceForSimilar < distanceForDifferent,
+ "Distance for similar should be the smallest.");
+ }
+
+ @Test
+ void testResetClearsCommands() {
+ RedisCommand cmd = new RedisCommand(
+ RedisCommand.RedisCommandType.EXISTS,
+ new String[]{"key"},
+ true,
+ 5
+ );
+ handler.handle(cmd);
+ assertFalse(handler.getEvaluatedRedisCommands().isEmpty());
+ handler.reset();
+ assertTrue(handler.getEvaluatedRedisCommands().isEmpty());
+ }
+}
\ No newline at end of file
diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/redis/RedisHandlerTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/redis/RedisHandlerTest.java
new file mode 100644
index 0000000000..023adc89b4
--- /dev/null
+++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/redis/RedisHandlerTest.java
@@ -0,0 +1,52 @@
+package org.evomaster.client.java.controller.internal.db.redis;
+
+import org.evomaster.client.java.instrumentation.RedisCommand;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class RedisHandlerTest {
+
+ private RedisHandler handler;
+
+ @BeforeEach
+ void setup() {
+ handler = new RedisHandler();
+ handler.setCalculateHeuristics(true);
+ }
+
+ @Test
+ void testHandleStoresCommands() {
+ RedisCommand cmd = new RedisCommand(
+ RedisCommand.RedisCommandType.EXISTS,
+ new String[]{"user:1"},
+ true,
+ 5
+ );
+ handler.handle(cmd);
+
+ assertTrue(handler.isExtractRedisExecution());
+ List evals = handler.getEvaluatedRedisCommands();
+
+ assertNotNull(evals);
+ assertFalse(evals.isEmpty());
+ }
+
+ @Test
+ void testResetClearsOperations() {
+ RedisCommand cmd = new RedisCommand(
+ RedisCommand.RedisCommandType.EXISTS,
+ new String[]{"user:1"},
+ true,
+ 5
+ );
+ handler.handle(cmd);
+ handler.reset();
+
+ List evals = handler.getEvaluatedRedisCommands();
+ assertTrue(evals.isEmpty());
+ }
+}
\ No newline at end of file
diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/redis/RedisHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/redis/RedisHeuristicsCalculatorTest.java
new file mode 100644
index 0000000000..26c3fa7a02
--- /dev/null
+++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/redis/RedisHeuristicsCalculatorTest.java
@@ -0,0 +1,278 @@
+package org.evomaster.client.java.controller.internal.db.redis;
+
+import org.evomaster.client.java.controller.redis.RedisHeuristicsCalculator;
+import org.evomaster.client.java.controller.redis.RedisInfo;
+import org.evomaster.client.java.instrumentation.RedisCommand;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class RedisHeuristicsCalculatorTest {
+
+ private RedisHeuristicsCalculator calculator;
+
+ @BeforeEach
+ void setup() {
+ calculator = new RedisHeuristicsCalculator();
+ }
+
+ @Test
+ void testKeysPatternExactMatch() {
+ RedisCommand cmd = new RedisCommand(
+ RedisCommand.RedisCommandType.KEYS,
+ new String[]{"key"},
+ true,
+ 5
+ );
+
+ List redisInfoList = new ArrayList<>();
+ redisInfoList.add(new RedisInfo("user:1"));
+ redisInfoList.add(new RedisInfo("user:2"));
+ redisInfoList.add(new RedisInfo("other"));
+
+ RedisDistanceWithMetrics result = calculator.computeDistance(cmd, redisInfoList);
+
+ assertEquals(0.0, result.getDistance(), 1e-6, "Pattern 'user*' should fully match 'user:1' and 'user:2'");
+ }
+
+ @Test
+ void testKeysPatternNoMatch() {
+ RedisCommand cmd = new RedisCommand(
+ RedisCommand.RedisCommandType.KEYS,
+ new String[]{"key"},
+ true,
+ 5
+ );
+
+ List redisInfoList = new ArrayList<>();
+ redisInfoList.add(new RedisInfo("user:1"));
+ redisInfoList.add(new RedisInfo("user:2"));
+
+ RedisDistanceWithMetrics result = calculator.computeDistance(cmd, redisInfoList);
+ assertEquals(1.0, result.getDistance(), 0.1, "Pattern with no matches should yield max distance 1");
+ assertEquals(2, result.getNumberOfEvaluatedKeys());
+ }
+
+ @Test
+ void testExistsCommandSimilarity() {
+ RedisCommand closeKey = new RedisCommand(
+ RedisCommand.RedisCommandType.EXISTS,
+ new String[]{"key"},
+ true,
+ 5
+ );
+
+ RedisCommand farKey = new RedisCommand(
+ RedisCommand.RedisCommandType.EXISTS,
+ new String[]{"key"},
+ true,
+ 5
+ );
+
+ List redisInfoList = new ArrayList<>();
+ redisInfoList.add(new RedisInfo("user:1"));
+ redisInfoList.add(new RedisInfo("user:2"));
+
+ RedisDistanceWithMetrics dClose = calculator.computeDistance(closeKey, redisInfoList);
+ RedisDistanceWithMetrics dFar = calculator.computeDistance(farKey, redisInfoList);
+
+ assertTrue(dClose.getDistance() < dFar.getDistance(),
+ "Closer key should have smaller distance.");
+ }
+
+ @Test
+ void testHGetFieldExists() {
+ RedisCommand cmd = new RedisCommand(
+ RedisCommand.RedisCommandType.HGET,
+ new String[]{"key", "key"},
+ true,
+ 3
+ );
+
+ List redisInfoList = new ArrayList<>();
+ redisInfoList.add(new RedisInfo("profile", true));
+ redisInfoList.add(new RedisInfo("users", false));
+
+ RedisDistanceWithMetrics result = calculator.computeDistance(cmd, redisInfoList);
+
+ assertEquals(0.0, result.getDistance(), 1e-6,
+ "Field 'name' exists, so distance must be 0");
+ assertTrue(result.getNumberOfEvaluatedKeys() > 0);
+ }
+
+ @Test
+ void testHGetFieldNotExists() {
+ RedisCommand cmd = new RedisCommand(
+ RedisCommand.RedisCommandType.HGET,
+ new String[]{"key", "key"},
+ true,
+ 3
+ );
+
+ List redisInfoList = new ArrayList<>();
+ redisInfoList.add(new RedisInfo("profile", false));
+
+ RedisDistanceWithMetrics result = calculator.computeDistance(cmd, redisInfoList);
+
+ assertTrue(result.getDistance() > 0.0, "Missing field should yield positive distance");
+ assertTrue(result.getNumberOfEvaluatedKeys() >= 1);
+ }
+
+ /**
+ * Distance in intersection between two given sets:
+ * - setA y setB share members → low distance
+ * - setC y setD have no members in common (but close) → greater distance
+ * - setE y setF have no members in common (very different) → the greatest distance
+ */
+ @Test
+ void testSInterSetsIntersectionAndNoIntersection() {
+ // Shared members.
+ RedisCommand cmdIntersect = new RedisCommand(
+ RedisCommand.RedisCommandType.SINTER,
+ new String[]{"key", "key"},
+ true,
+ 1
+ );
+
+ List redisInfoListIntersection = new ArrayList<>();
+ redisInfoListIntersection.add(new RedisInfo("setA", "set", new HashSet<>(Arrays.asList("a", "b", "c"))));
+ redisInfoListIntersection.add(new RedisInfo("setB", "set", new HashSet<>(Arrays.asList("b", "c", "d"))));
+
+ RedisDistanceWithMetrics dIntersect = calculator.computeDistance(cmdIntersect, redisInfoListIntersection);
+ assertEquals(0.0, dIntersect.getDistance(),
+ "Set intersection distance equals 0.0 when sets share members.");
+
+ // No members in common
+ RedisCommand cmdNoIntersect = new RedisCommand(
+ RedisCommand.RedisCommandType.SINTER,
+ new String[]{"key", "key"},
+ true,
+ 1
+ );
+
+ List redisInfoListNoIntersection = new ArrayList<>();
+ redisInfoListNoIntersection.add(new RedisInfo("setC", "set", new HashSet<>(Arrays.asList("a", "b"))));
+ redisInfoListNoIntersection.add(new RedisInfo("setD", "set", new HashSet<>(Arrays.asList("c", "d"))));
+
+ RedisDistanceWithMetrics dNoIntersect = calculator.computeDistance(cmdNoIntersect, redisInfoListNoIntersection);
+
+ assertTrue(dNoIntersect.getDistance() > 0.0,
+ "With disjoint sets, distance must be greater than zero.");
+ assertTrue(dIntersect.getDistance() < dNoIntersect.getDistance(),
+ "Sets with common elements should yield smaller distance");
+
+ // No members in common, greater distance
+ RedisCommand cmdNoIntersectFarDistance = new RedisCommand(
+ RedisCommand.RedisCommandType.SINTER,
+ new String[]{"key", "key"},
+ true,
+ 1
+ );
+
+ List redisInfoListGreaterDisjoint = new ArrayList<>();
+ redisInfoListGreaterDisjoint.add(new RedisInfo("setC", "set", new HashSet<>(Arrays.asList("a", "b"))));
+ redisInfoListGreaterDisjoint.add(new RedisInfo("setD", "set", new HashSet<>(Arrays.asList("y", "z"))));
+
+ RedisDistanceWithMetrics dNoIntersectFarDistance = calculator.computeDistance(cmdNoIntersectFarDistance, redisInfoListGreaterDisjoint);
+
+ assertTrue(dNoIntersectFarDistance.getDistance() > 0.0,
+ "With disjoint sets, distance must be greater than zero.");
+ assertTrue(dNoIntersect.getDistance() < dNoIntersectFarDistance.getDistance(),
+ "Sets with close elements should yield smaller distance");
+ }
+
+ /**
+ * Distance in intersection between several sets.
+ * When there's no intersection between all of them, distance should be greater as fewer intersections are possible.
+ */
+ @Test
+ void testSInterSeveralSets() {
+ RedisCommand cmdIntersect = new RedisCommand(
+ RedisCommand.RedisCommandType.SINTER,
+ new String[]{"key", "key", "key", "key"},
+ true,
+ 1
+ );
+
+ List redisInfoListLessDistance = new ArrayList<>();
+ redisInfoListLessDistance.add(new RedisInfo("setA", "set", new HashSet<>(Arrays.asList("a", "b", "c"))));
+ redisInfoListLessDistance.add(new RedisInfo("setB", "set", new HashSet<>(Arrays.asList("b", "c"))));
+ redisInfoListLessDistance.add(new RedisInfo("setC", "set", new HashSet<>(Arrays.asList("c", "d"))));
+ redisInfoListLessDistance.add(new RedisInfo("setD", "set", new HashSet<>(Arrays.asList("d", "e"))));
+
+ RedisDistanceWithMetrics dIntersectLessDistance = calculator.computeDistance(cmdIntersect, redisInfoListLessDistance);
+ assertTrue(dIntersectLessDistance.getDistance() > 0.0,
+ "With disjoint sets, distance must be greater than zero.");
+
+ List redisInfoListMoreDistance = new ArrayList<>();
+ redisInfoListMoreDistance.add(new RedisInfo("setA", "set", new HashSet<>(Arrays.asList("a", "b", "c"))));
+ redisInfoListMoreDistance.add(new RedisInfo("setB", "set", new HashSet<>(Arrays.asList("b", "c"))));
+ redisInfoListMoreDistance.add(new RedisInfo("setC", "set", new HashSet<>(Arrays.asList("d", "e"))));
+ redisInfoListMoreDistance.add(new RedisInfo("setD", "set", new HashSet<>(Arrays.asList("f", "g"))));
+
+ RedisDistanceWithMetrics dIntersectMoreDistance = calculator.computeDistance(cmdIntersect, redisInfoListMoreDistance);
+ assertTrue(dIntersectMoreDistance.getDistance() > 0.0,
+ "With disjoint sets, distance must be greater than zero.");
+
+ assertTrue(dIntersectMoreDistance.getDistance() > dIntersectLessDistance.getDistance(),
+ "Distance should be greater as fewer intersections are possible.");
+ }
+
+ @Test
+ void testSMembersSimilarity() {
+ RedisCommand similar = new RedisCommand(
+ RedisCommand.RedisCommandType.SMEMBERS,
+ new String[]{"key"},
+ true,
+ 2
+ );
+ RedisCommand different = new RedisCommand(
+ RedisCommand.RedisCommandType.SMEMBERS,
+ new String[]{"key"},
+ true,
+ 2
+ );
+
+ List redisInfoList = new ArrayList<>();
+ redisInfoList.add(new RedisInfo("user:setA"));
+ redisInfoList.add(new RedisInfo("user:setB"));
+ redisInfoList.add(new RedisInfo("profile:set"));
+
+ double dSimilar = calculator.computeDistance(similar, redisInfoList).getDistance();
+ double dDifferent = calculator.computeDistance(different, redisInfoList).getDistance();
+
+ assertTrue(dSimilar < dDifferent,
+ "SMEMBERS with similar keys should yield smaller distance");
+ }
+
+ @Test
+ void testGetCommandSimilarity() {
+ RedisCommand similar = new RedisCommand(
+ RedisCommand.RedisCommandType.GET,
+ new String[]{"key"},
+ true,
+ 1
+ );
+
+ RedisCommand different = new RedisCommand(
+ RedisCommand.RedisCommandType.GET,
+ new String[]{"key"},
+ true,
+ 1
+ );
+
+ List redisInfoList = new ArrayList<>();
+ redisInfoList.add(new RedisInfo("session:1235"));
+ redisInfoList.add(new RedisInfo("config"));
+ redisInfoList.add(new RedisInfo("log"));
+
+ double dSimilar = calculator.computeDistance(similar, redisInfoList).getDistance();
+ double dDifferent = calculator.computeDistance(different, redisInfoList).getDistance();
+
+ assertTrue(dSimilar < dDifferent,
+ "GET with similar keys should yield smaller distance");
+ }
+}
\ No newline at end of file
diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/redis/RedisUtilsTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/redis/RedisUtilsTest.java
new file mode 100644
index 0000000000..09ef46738a
--- /dev/null
+++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/redis/RedisUtilsTest.java
@@ -0,0 +1,112 @@
+package org.evomaster.client.java.controller.internal.db.redis;
+
+import org.evomaster.client.java.controller.redis.RedisUtils;
+import org.junit.jupiter.api.Test;
+import java.util.regex.Pattern;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class RedisUtilsTest {
+
+ @Test
+ void testNullAndEmptyPatterns() {
+ assertEquals(".*", RedisUtils.redisPatternToRegex(null));
+ assertEquals(".*", RedisUtils.redisPatternToRegex(""));
+ }
+
+ @Test
+ void testWildcardStar() {
+ String regex = RedisUtils.redisPatternToRegex("foo*");
+ assertTrue("foo".matches(regex));
+ assertTrue("foobar".matches(regex));
+ assertTrue("foobarbaz".matches(regex));
+ assertFalse("fo".matches(regex));
+ }
+
+ @Test
+ void testWildcardQuestionMark() {
+ String regex = RedisUtils.redisPatternToRegex("h?llo");
+ assertTrue("hello".matches(regex));
+ assertTrue("hallo".matches(regex));
+ assertFalse("hllo".matches(regex));
+ assertFalse("heallo".matches(regex));
+ }
+
+ @Test
+ void testBracketSimpleSet() {
+ String regex = RedisUtils.redisPatternToRegex("h[ae]llo");
+ assertTrue("hello".matches(regex));
+ assertTrue("hallo".matches(regex));
+ assertFalse("hollo".matches(regex));
+ }
+
+ @Test
+ void testBracketNegatedSet() {
+ String regex = RedisUtils.redisPatternToRegex("h[^e]llo");
+ assertTrue("hallo".matches(regex));
+ assertTrue("hollo".matches(regex));
+ assertFalse("hello".matches(regex));
+ }
+
+ @Test
+ void testBracketRange() {
+ String regex = RedisUtils.redisPatternToRegex("file[0-9]");
+ assertTrue("file1".matches(regex));
+ assertTrue("file9".matches(regex));
+ assertFalse("filea".matches(regex));
+ }
+
+ @Test
+ void testEscapingRegexMetacharacters() {
+ String regex = RedisUtils.redisPatternToRegex("price+(usd)");
+ assertTrue("price+(usd)".matches(regex));
+ assertFalse("price-usd".matches(regex));
+ }
+
+ @Test
+ void testComplexPatternWithStarAndBrackets() {
+ String regex = RedisUtils.redisPatternToRegex("user:[0-9]*");
+ assertTrue("user:123".matches(regex));
+ assertTrue("user:0".matches(regex));
+ assertTrue("user:9999".matches(regex));
+ assertFalse("user:a".matches(regex));
+ assertFalse("usr:123".matches(regex));
+ }
+
+ @Test
+ void testNestedEscapesAndBrackets() {
+ String regex = RedisUtils.redisPatternToRegex("a\\[test\\]");
+ assertEquals("^a\\[test\\]$", regex);
+ System.out.println(regex);
+ }
+
+ @Test
+ void testEscapedBrackets() {
+ String regex = RedisUtils.redisPatternToRegex("a\\[test");
+ assertEquals("^a\\[test$", regex);
+ regex = RedisUtils.redisPatternToRegex("test\\]");
+ assertEquals("^test\\]$", regex);
+ }
+
+ @Test
+ void testAnchorsAdded() {
+ String regex = RedisUtils.redisPatternToRegex("foo*");
+ assertTrue(regex.startsWith("^"));
+ assertTrue(regex.endsWith("$"));
+ }
+
+ @Test
+ void testMultipleWildcards() {
+ String regex = RedisUtils.redisPatternToRegex("*mid*dle*");
+ assertTrue("middle".matches(regex));
+ assertTrue("amidxxdlezzz".matches(regex));
+ assertFalse("middl".matches(regex));
+ }
+
+ @Test
+ void testSpecialCharactersEscapedProperly() {
+ String regex = RedisUtils.redisPatternToRegex("a.b+c{d}|e^f$g");
+ assertTrue(Pattern.matches(regex, "a.b+c{d}|e^f$g"));
+ assertFalse(Pattern.matches(regex, "ab+c{d}|e^f$g"));
+ }
+}
\ No newline at end of file
diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/RedisCommand.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/RedisCommand.java
index 95a526ee47..abe10e088e 100644
--- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/RedisCommand.java
+++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/RedisCommand.java
@@ -1,7 +1,9 @@
package org.evomaster.client.java.instrumentation;
import java.io.Serializable;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
/**
* Info related to Redis commands execution.
@@ -11,47 +13,153 @@ public class RedisCommand implements Serializable {
* Redis commands we'd like to capture. Extendable to any other command in Redis that may be of interest.
*/
public enum RedisCommandType {
+ /**
+ * Removes the specified keys. A key is ignored if it does not exist.
+ * Integer reply: the number of keys that were removed.
+ * DEL Documentation
+ */
+ DEL("del", "mixed", false),
+ /**
+ * Invoke the execution of a server-side Lua script.
+ * The return value depends on the script that was executed.
+ * EVAL Documentation
+ */
+ EVAL("eval", "script", false),
+ /**
+ * Evaluate a script from the server's cache by its SHA1 digest.
+ * The return value depends on the script that was executed.
+ * EVALSHA Documentation
+ */
+ EVALSHA("evalsha", "script", false),
+ /**
+ * Returns if key exists.
+ * Integer reply: the number of keys that exist from those specified as arguments.
+ * EXISTS Documentation
+ */
+ EXISTS("exists", "mixed", true),
/**
* Get the value of key.
* GET Documentation
*/
- GET,
+ GET("get", "string", true),
/**
* Returns the value associated with field in the hash stored at key.
* HGET Documentation
*/
- HGET,
+ HGET("hget", "hash", true),
/**
* Returns all fields and values of the hash stored at key.
* HGETALL Documentation
*/
- HGETALL,
+ HGETALL("hgetall", "hash", true),
/**
- * Returns all keys matching pattern.
- * KEYS Documentation
+ * Increments the number stored at key by one.
+ * If the key does not exist, it is set to 0 before performing the operation.
+ * An error is returned if the key contains a value of the wrong type
+ * or contains a string that can not be represented as integer.
+ * This operation is limited to 64-bit signed integers.
+ * INCR Documentation
*/
- KEYS,
+ INCR("incr", "string", false),
/**
- * Returns the members of the set resulting from the intersection of all the given sets.
- * SINTER Documentation
+ * Returns all keys matching pattern.
+ * KEYS Documentation
*/
- SINTER,
+ KEYS("keys", "none", true),
/**
* Set key to hold the string value. If key already holds a value, it is overwritten, regardless of its type.
* Any previous time to live associated with the key is discarded on successful SET operation.
* SET Documentation
*/
- SET,
+ SET("set", "string", false),
+ /**
+ * Posts a message to the given channel.
+ * Integer reply: the number of clients that the message was sent to.
+ * PUBLISH Documentation
+ */
+ PUBLISH("publish", "pubsub", false),
+ /**
+ * Add the specified members to the set stored at key.
+ * Specified members that are already a member of this set are ignored.
+ * If key does not exist, a new set is created before adding the specified members.
+ * An error is returned when the value stored at key is not a set.
+ * SADD Documentation
+ */
+ SADD("sadd", "set", false),
+ /**
+ * Set key to hold the string value and set key to timeout after a given number of seconds.
+ * SETEX Documentation
+ */
+ SETEX("setex", "string", false),
+ /**
+ * Returns the members of the set resulting from the intersection of all the given sets.
+ * SINTER Documentation
+ */
+ SINTER("sinter", "set", true),
/**
* Returns all the members of the set value stored at key.
* This has the same effect as running SINTER with one argument key.
* SMEMBERS Documentation
*/
- SMEMBERS,
+ SMEMBERS("smembers", "set", true),
+ /**
+ * Removes and returns one or more random members from the set value store at key.
+ * Nil reply: if the key does not exist.
+ * Bulk string reply: when called without the count argument, the removed member.
+ * Array reply: when called with the count argument, a list of the removed members.
+ * SPOP Documentation
+ */
+ SPOP("spop", "set", false),
+ /**
+ * Remove the specified members from the set stored at key.
+ * Specified members that are not a member of this set are ignored.
+ * If key does not exist, it is treated as an empty set and this command returns 0.
+ * An error is returned when the value stored at key is not a set.
+ * SREM Documentation
+ */
+ SREM("srem", "set", false),
+ /**
+ * Subscribes the client to the specified channels.
+ * When successful, this command doesn't return anything.
+ * Instead, for each channel, one message with the first element being the string subscribe is pushed
+ * as a confirmation that the command succeeded.
+ * SUBSCRIBE Documentation
+ */
+ SUBSCRIBE("subscribe", "pubsub", false),
+ /**
+ * Unsubscribes the client from the given channels, or from all of them if none is given.
+ * When successful, this command doesn't return anything.
+ * Instead, for each channel, one message with the first element being the string unsubscribe is pushed
+ * as a confirmation that the command succeeded.
+ * UNSUBSCRIBE Documentation
+ */
+ UNSUBSCRIBE("unsubscribe", "pubsub", false),
/**
* Default unregistered command value.
*/
- OTHER
+ OTHER("other", "none", false);
+
+ private final String label;
+ private final String dataType;
+ private final boolean calculateHeuristic;
+
+ RedisCommandType(String label, String dataType, boolean shouldCalculateHeuristic) {
+ this.label = label;
+ this.dataType = dataType;
+ this.calculateHeuristic = shouldCalculateHeuristic;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ public String getDataType() {
+ return dataType;
+ }
+
+ public boolean shouldCalculateHeuristic() {
+ return calculateHeuristic;
+ }
}
/**
@@ -93,6 +201,14 @@ public String[] getArgs() {
return args;
}
+ public List extractArgs(){
+ List parameters = new ArrayList<>();
+ for(String arg : args){
+ parameters.add(arg.substring(arg.indexOf('<')+1, arg.indexOf('>')));
+ }
+ return parameters;
+ }
+
public boolean getSuccessfullyExecuted() {
return successfullyExecuted;
}
@@ -100,8 +216,4 @@ public boolean getSuccessfullyExecuted() {
public long getExecutionTime() {
return executionTime;
}
-
- public boolean isHashCommand() {
- return type.equals(RedisCommandType.HGET);
- }
}
diff --git a/pom.xml b/pom.xml
index 53c476ac9c..922263991b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -174,6 +174,7 @@
3.1.0
1.0.0
2.6.6
+ 6.1.4.RELEASE
@@ -599,6 +600,13 @@
${org.mongodb.version}
+
+