diff --git a/client-java/controller/pom.xml b/client-java/controller/pom.xml index 1405db0454..989ae4457f 100644 --- a/client-java/controller/pom.xml +++ b/client-java/controller/pom.xml @@ -199,6 +199,11 @@ mongodb-driver-sync test + + io.lettuce + lettuce-core + test + 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} + + + io.lettuce + lettuce-core + ${io.lettuce.core.version} + + org.opensearch.client