diff --git a/.gitignore b/.gitignore index a9f267fc01..fad0a5c87e 100644 --- a/.gitignore +++ b/.gitignore @@ -105,6 +105,7 @@ Migrations/ /core-tests/e2e-tests/spring/spring-web/target/ /core-tests/e2e-tests/spring/spring-rest-mongo/target/ /core-tests/e2e-tests/spring/spring-rest-opensearch/target/ +/core-tests/e2e-tests/spring/spring-rest-redis/target/ /core-tests/client-java/target/ /core-tests/client-java/dependencies/target/ /core-tests/client-java/sql-dto/target/ diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/ExtraHeuristicEntryDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/ExtraHeuristicEntryDto.java index 578199ea95..2a089f3ff3 100644 --- a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/ExtraHeuristicEntryDto.java +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/ExtraHeuristicEntryDto.java @@ -9,9 +9,9 @@ public class ExtraHeuristicEntryDto implements Serializable { /** * The type of extra heuristic. - * Note: for the moment, we only have heuristics on SQL, MONGO and OPENSEARCH commands + * Note: for the moment, we only have heuristics on SQL, MONGO, OPENSEARCH and REDIS commands */ - public enum Type {SQL, MONGO, OPENSEARCH} + public enum Type {SQL, MONGO, OPENSEARCH, REDIS} /** * Should we try to minimize or maximize the heuristic? 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 5ee0cc89df..4b835548a9 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 @@ -5,6 +5,7 @@ import org.evomaster.client.java.controller.api.dto.database.operations.MongoInsertionDto; import org.evomaster.client.java.controller.api.dto.database.operations.MongoInsertionResultsDto; import org.evomaster.client.java.controller.api.dto.problem.rpc.ScheduleTaskInvocationResultDto; +import org.evomaster.client.java.controller.redis.ReflectionBasedRedisClient; import org.evomaster.client.java.sql.DbCleaner; import org.evomaster.client.java.sql.DbSpecification; @@ -181,7 +182,7 @@ default void extractRPCSchema(){} default Object getOpenSearchConnection() {return null;} - default Object getRedisConnection() {return null;} + default ReflectionBasedRedisClient getRedisConnection() {return null;} /** *

diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/EMController.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/EMController.java index ea08188418..7533738464 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/EMController.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/EMController.java @@ -388,6 +388,7 @@ public Response runSut(SutRunDto dto, @Context HttpServletRequest httpServletReq noKillSwitch(() -> sutController.registerOrExecuteInitSqlCommandsIfNeeded(true)); noKillSwitch(() -> sutController.initMongoHandler()); noKillSwitch(() -> sutController.initOpenSearchHandler()); + noKillSwitch(() -> sutController.initRedisHandler()); } else { //TODO as starting should be blocking, need to check //if initialized, and wait if not diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/SutController.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/SutController.java index e6f64b89bf..0b8a14b8f9 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/SutController.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/internal/SutController.java @@ -29,6 +29,8 @@ import org.evomaster.client.java.controller.api.dto.problem.rpc.*; import org.evomaster.client.java.controller.api.dto.problem.rpc.RPCTestDto; import org.evomaster.client.java.controller.internal.db.OpenSearchHandler; +import org.evomaster.client.java.controller.internal.db.redis.RedisHandler; +import org.evomaster.client.java.controller.redis.ReflectionBasedRedisClient; import org.evomaster.client.java.sql.DbCleaner; import org.evomaster.client.java.sql.SqlScriptRunner; import org.evomaster.client.java.sql.SqlScriptRunnerCached; @@ -90,6 +92,8 @@ public abstract class SutController implements SutHandler, CustomizationHandler private final OpenSearchHandler openSearchHandler = new OpenSearchHandler(); + private final RedisHandler redisHandler = new RedisHandler(); + private Server controllerServer; /** @@ -322,10 +326,13 @@ public final void initSqlHandler() { } public final void initMongoHandler() { - // This is needed because the replacement use to get this info occurs during the start of the SUT. Object connection = getMongoConnection(); mongoHandler.setMongoClient(connection); + // Spring MongoDB repositories capture document type metadata during SUT startup. + // We must extract this schema info from MappingMongoEntityInformation instances + // created during initialization, as this mapping is not accessible from the + // standard MongoDB driver collections later on. List list = getAdditionalInfoList(); if(!list.isEmpty()) { AdditionalInfo last = list.get(list.size() - 1); @@ -346,6 +353,17 @@ public final void initOpenSearchHandler() { } } + public final void initRedisHandler() { + ReflectionBasedRedisClient connection = getRedisConnection(); + redisHandler.setRedisClient(connection); + + List list = getAdditionalInfoList(); + if (!list.isEmpty()) { + AdditionalInfo last = list.get(list.size() - 1); + last.getRedisCommandData().forEach(redisHandler::handle); + } + } + /** * TODO further handle multiple connections * @return sql connection if there exists @@ -388,7 +406,8 @@ public final ExtraHeuristicsDto computeExtraHeuristics(boolean queryFromDatabase ExtraHeuristicsDto dto = new ExtraHeuristicsDto(); - if (isSQLHeuristicsComputationAllowed() || isMongoHeuristicsComputationAllowed() || isOpenSearchHeuristicsComputationAllowed()) { + if (isSQLHeuristicsComputationAllowed() || isMongoHeuristicsComputationAllowed() + || isOpenSearchHeuristicsComputationAllowed() || isRedisHeuristicsComputationAllowed()) { List additionalInfoList = getAdditionalInfoList(); if (isSQLHeuristicsComputationAllowed()) { @@ -397,10 +416,12 @@ public final ExtraHeuristicsDto computeExtraHeuristics(boolean queryFromDatabase if (isMongoHeuristicsComputationAllowed()) { computeMongoHeuristics(dto, additionalInfoList); } - if (isOpenSearchHeuristicsComputationAllowed()) { computeOpenSearchHeuristics(dto, additionalInfoList); } + if (isRedisHeuristicsComputationAllowed()) { + computeRedisHeuristics(dto, additionalInfoList); + } } return dto; } @@ -417,6 +438,10 @@ private boolean isOpenSearchHeuristicsComputationAllowed() { return openSearchHandler.isCalculateHeuristics(); } + private boolean isRedisHeuristicsComputationAllowed() { + return redisHandler.isCalculateHeuristics(); + } + private void computeSQLHeuristics(ExtraHeuristicsDto dto, List additionalInfoList, boolean queryFromDatabase) { /* TODO refactor, once we move SQL analysis into Core @@ -528,6 +553,34 @@ public final void computeOpenSearchHeuristics(ExtraHeuristicsDto dto, List additionalInfoList){ + if(redisHandler.isCalculateHeuristics()){ + if(!additionalInfoList.isEmpty()) { + AdditionalInfo last = additionalInfoList.get(additionalInfoList.size() - 1); + last.getRedisCommandData().forEach(it -> { + try { + redisHandler.handle(it); + } catch (Exception e){ + SimpleLogger.error("FAILED TO HANDLE REDIS COMMAND", e); + assert false; + } + }); + } + + redisHandler.getEvaluatedRedisCommands().stream() + .map(p -> + new ExtraHeuristicEntryDto( + ExtraHeuristicEntryDto.Type.REDIS, + ExtraHeuristicEntryDto.Objective.MINIMIZE_TO_ZERO, + p.getRedisCommand().toString(), + p.getRedisDistanceWithMetrics().getDistance(), + p.getRedisDistanceWithMetrics().getNumberOfEvaluatedKeys(), + false + )) + .forEach(h -> dto.heuristics.add(h)); + } + } + /** * handle specified init sql script after SUT is started. */ 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 index bc737713a7..e41df05736 100644 --- 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 @@ -1,7 +1,7 @@ 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.ReflectionBasedRedisClient; import org.evomaster.client.java.controller.redis.RedisHeuristicsCalculator; import org.evomaster.client.java.controller.redis.RedisInfo; import org.evomaster.client.java.instrumentation.RedisCommand; @@ -32,14 +32,9 @@ public class RedisHandler { private volatile boolean calculateHeuristics; /** - * Whether to use execution's info or not + * The client must be created given both host and port for Redis DB. */ - private volatile boolean extractRedisExecution; - - /** - * The client must be created through a connection factory. - */ - private RedisClient redisClient = null; + private ReflectionBasedRedisClient redisClient = null; private final RedisHeuristicsCalculator calculator = new RedisHeuristicsCalculator(new TaintHandlerExecutionTracer()); @@ -49,7 +44,6 @@ public class RedisHandler { public RedisHandler() { operations = new ArrayList<>(); - extractRedisExecution = true; calculateHeuristics = true; } @@ -62,22 +56,12 @@ 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); - } + operations.add(info); } public List getEvaluatedRedisCommands() { @@ -92,7 +76,7 @@ public List getEvaluatedRedisCommands() { return evaluatedRedisCommands; } - private RedisDistanceWithMetrics computeDistance(RedisCommand redisCommand, RedisClient redisClient) { + private RedisDistanceWithMetrics computeDistance(RedisCommand redisCommand, ReflectionBasedRedisClient redisClient) { RedisCommand.RedisCommandType type = redisCommand.getType(); try { switch (type) { @@ -138,7 +122,7 @@ private RedisDistanceWithMetrics computeDistance(RedisCommand redisCommand, Redi } } - private List createRedisInfoForIntersection(List keys, RedisClient redisClient) { + private List createRedisInfoForIntersection(List keys, ReflectionBasedRedisClient redisClient) { List redisData = new ArrayList<>(); keys.forEach( key -> redisData.add(new RedisInfo(key, redisClient.getType(key), redisClient.getSetMembers(key)) @@ -146,7 +130,7 @@ private List createRedisInfoForIntersection(List keys, RedisC return redisData; } - private List createRedisInfoForAllKeys(RedisClient redisClient) { + private List createRedisInfoForAllKeys(ReflectionBasedRedisClient redisClient) { Set keys = redisClient.getAllKeys(); List redisData = new ArrayList<>(); keys.forEach( @@ -155,21 +139,21 @@ private List createRedisInfoForAllKeys(RedisClient redisClient) { return redisData; } - private List createRedisInfoForKeysByType(String type, RedisClient redisClient) { + private List createRedisInfoForKeysByType(String type, ReflectionBasedRedisClient 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) { + private List createRedisInfoForKeysByField(String field, ReflectionBasedRedisClient redisClient) { Set keys = redisClient.getKeysByType(REDIS_HASH_TYPE); List redisData = new ArrayList<>(); - keys.forEach(key -> redisData.add(new RedisInfo(key, redisClient.hashFieldExists(key, field)))); + keys.forEach(key -> redisData.add(new RedisInfo(key, redisClient.getHashFields(key)))); return redisData; } - public void setRedisClient(RedisClient redisClient) { + public void setRedisClient(ReflectionBasedRedisClient redisClient) { this.redisClient = redisClient; } } 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 index 79085258cd..18e3910692 100644 --- 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 @@ -53,7 +53,8 @@ public RedisDistanceWithMetrics computeDistance(RedisCommand redisCommand, List< case HGET: { String key = redisCommand.extractArgs().get(0); - return calculateDistanceForFieldInHash(key, redisInfo); + String field = redisCommand.extractArgs().get(1); + return calculateDistanceForFieldInHash(key, field, redisInfo); } case SINTER: { @@ -89,8 +90,12 @@ private RedisDistanceWithMetrics calculateDistanceForPattern( return new RedisDistanceWithMetrics(MAX_REDIS_DISTANCE, 0); } for (RedisInfo k : keys) { + String key = k.getKey(); double d = TruthnessUtils.normalizeValue( - RegexDistanceUtils.getStandardDistance(k.getKey(), redisPatternToRegex(pattern))); + RegexDistanceUtils.getStandardDistance(key, regex)); + if (taintHandler != null) { + taintHandler.handleTaintForRegex(key, regex); + } minDist = Math.min(minDist, d); eval++; if (d == 0) return new RedisDistanceWithMetrics(0, eval); @@ -119,8 +124,12 @@ private RedisDistanceWithMetrics calculateDistanceForKeyMatch( for (RedisInfo k : candidateKeys) { try { - long rawDist = DistanceHelper.getLeftAlignmentDistance(targetKey, k.getKey()); + String key = k.getKey(); + long rawDist = DistanceHelper.getLeftAlignmentDistance(targetKey, key); double normDist = TruthnessUtils.normalizeValue(rawDist); + if (taintHandler != null) { + taintHandler.handleTaintForStringEquals(targetKey, key, false); + } minDist = Math.min(minDist, normDist); evaluated++; @@ -144,6 +153,7 @@ private RedisDistanceWithMetrics calculateDistanceForKeyMatch( */ private RedisDistanceWithMetrics calculateDistanceForFieldInHash( String targetKey, + String targetField, List keys ) { if (keys.isEmpty()) { @@ -155,12 +165,13 @@ private RedisDistanceWithMetrics calculateDistanceForFieldInHash( for (RedisInfo k : keys) { try { - long keyDist = DistanceHelper.getLeftAlignmentDistance(targetKey, k.getKey()); - - double fieldDist = k.hasField() ? 0d : MAX_REDIS_DISTANCE; - + String key = k.getKey(); + long keyDist = DistanceHelper.getLeftAlignmentDistance(targetKey, key); + double fieldDist = calculateDistanceForField(targetField, k.getFields().keySet()); double combined = TruthnessUtils.normalizeValue(keyDist + fieldDist); - + if (taintHandler != null) { + taintHandler.handleTaintForStringEquals(targetKey, key, false); + } minDist = Math.min(minDist, combined); evaluated++; @@ -175,6 +186,34 @@ private RedisDistanceWithMetrics calculateDistanceForFieldInHash( return new RedisDistanceWithMetrics(minDist, evaluated); } + /** + * Computes the distance of target field to each field in hash. + * + * @param targetField Field searched in query. + * @param fields Fields in hash. + * @return double + */ + private double calculateDistanceForField(String targetField, Set fields) { + if (fields.isEmpty()) { + return Double.MAX_VALUE; + } + + double minDist = Double.MAX_VALUE; + + for (String field : fields) { + try { + long fieldDist = DistanceHelper.getLeftAlignmentDistance(targetField, field); + if (taintHandler != null) { + taintHandler.handleTaintForStringEquals(targetField, field, false); + } + minDist = Math.min(minDist, fieldDist); + } catch (Exception ex) { + SimpleLogger.uniqueWarn("Failed FIELD distance on " + targetField + ": " + ex.getMessage()); + } + } + return minDist; + } + /** * Computes the distance of a given intersection considering the keys for the given sets. * @@ -237,6 +276,9 @@ private double computeSetIntersectionDistance(Set s1, Set s2) { for (String a : s1) { for (String b : s2) { long raw = DistanceHelper.getLeftAlignmentDistance(a, b); + if (taintHandler != null) { + taintHandler.handleTaintForStringEquals(a, b, false); + } double norm = TruthnessUtils.normalizeValue(raw); min = Math.min(min, norm); if (min == 0) return 0; 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 index 51c7fa2a41..df3f531d6d 100644 --- 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 @@ -1,5 +1,6 @@ package org.evomaster.client.java.controller.redis; +import java.util.Map; import java.util.Set; /** @@ -10,16 +11,16 @@ public class RedisInfo { private String key; private String type; - private boolean hasField; + private Map fields; private Set members; public RedisInfo(String key) { this.key = key; } - public RedisInfo(String key, boolean hasField) { + public RedisInfo(String key, Map fields) { this.key = key; - this.hasField = hasField; + this.fields = fields; } public RedisInfo(String key, String type, Set members) { @@ -36,11 +37,11 @@ public String getType() { return type; } - public boolean hasField() { - return hasField; - } - public Set getMembers() { return members; } + + public Map getFields() { + return fields; + } } 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/ReflectionBasedRedisClient.java similarity index 59% rename from client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/RedisClient.java rename to client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/ReflectionBasedRedisClient.java index 2969e17b46..ec10f6c7b6 100644 --- 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/ReflectionBasedRedisClient.java @@ -1,5 +1,7 @@ package org.evomaster.client.java.controller.redis; +import org.evomaster.client.java.utils.SimpleLogger; + import java.lang.reflect.Method; import java.util.*; import java.util.stream.Collectors; @@ -8,28 +10,44 @@ * RedisClient that uses Lettuce dynamically via reflection, avoiding * compile-time dependency on Spring or Lettuce. */ -public class RedisClient { +public class ReflectionBasedRedisClient { - private final Object redisClient; // io.lettuce.core.RedisClient + private final Object lettuceClient; // 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) { + private static final String CLOSE_METHOD = "close"; + private static final String CONNECT_METHOD = "connect"; + private static final String CREATE_METHOD = "create"; + private static final String FLUSHALL_METHOD = "flushall"; + private static final String GET_METHOD = "get"; + private static final String HGETALL_METHOD = "hgetall"; + private static final String HSET_METHOD = "hset"; + private static final String KEYS_METHOD = "keys"; + private static final String SET_METHOD = "set"; + private static final String SHUTDOWN_METHOD = "shutdown"; + private static final String SMEMBERS_METHOD = "smembers"; + private static final String SYNC_METHOD = "sync"; + private static final String TYPE_METHOD = "type"; + + public ReflectionBasedRedisClient(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); + Method createUri = redisURIClass.getMethod(CREATE_METHOD, String.class); Object uri = createUri.invoke(null, "redis://" + host + ":" + port); - Method createClient = redisClientClass.getMethod("create", redisURIClass); - this.redisClient = createClient.invoke(null, uri); + SimpleLogger.debug("Connecting to Redis with PORT: " + port); + + Method createClient = redisClientClass.getMethod(CREATE_METHOD, redisURIClass); + this.lettuceClient = createClient.invoke(null, uri); - Method connectMethod = redisClientClass.getMethod("connect"); - this.connection = connectMethod.invoke(redisClient); + Method connectMethod = redisClientClass.getMethod(CONNECT_METHOD); + this.connection = connectMethod.invoke(lettuceClient); Class statefulConnClass = Class.forName("io.lettuce.core.api.StatefulRedisConnection"); - Method syncMethod = statefulConnClass.getMethod("sync"); + Method syncMethod = statefulConnClass.getMethod(SYNC_METHOD); this.syncCommands = syncMethod.invoke(connection); } catch (Exception e) { @@ -40,29 +58,29 @@ public RedisClient(String host, int port) { public void close() { try { if (connection != null) { - Method close = connection.getClass().getMethod("close"); + Method close = connection.getClass().getMethod(CLOSE_METHOD); close.invoke(connection); } - if (redisClient != null) { - Method shutdown = redisClient.getClass().getMethod("shutdown"); - shutdown.invoke(redisClient); + if (lettuceClient != null) { + Method shutdown = lettuceClient.getClass().getMethod(SHUTDOWN_METHOD); + shutdown.invoke(lettuceClient); } } catch (Exception ignored) {} } /** Equivalent to SET key value */ public void setValue(String key, String value) { - invoke("set", key, value); + invoke(SET_METHOD, key, value); } /** Equivalent to GET key */ public String getValue(String key) { - return (String) invoke("get", key); + return (String) invoke(GET_METHOD, key); } /** Equivalent to KEYS * */ public Set getAllKeys() { - Object result = invoke("keys", "*"); + Object result = invoke(KEYS_METHOD, "*"); if (result instanceof Collection) return new HashSet<>((Collection) result); return Collections.emptySet(); @@ -70,24 +88,18 @@ public Set getAllKeys() { /** Equivalent to TYPE key */ public String getType(String key) { - Object result = invoke("type", key); + Object result = invoke(TYPE_METHOD, 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; + invoke(HSET_METHOD, key, field, value); } /** SMEMBERS key */ public Set getSetMembers(String key) { - Object result = invoke("smembers", key); + Object result = invoke(SMEMBERS_METHOD, key); if (result instanceof Collection) return new HashSet<>((Collection) result); return Collections.emptySet(); @@ -125,6 +137,11 @@ public Set getKeysByType(String expectedType) { } public void flushAll() { - invoke("flushall"); + invoke(FLUSHALL_METHOD); + } + + public Map getHashFields(String key) { + Object result = invoke(HGETALL_METHOD, key); + return (Map) result; } } \ No newline at end of file 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 index 3bf0baa234..95e1be94a9 100644 --- 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 @@ -1,7 +1,7 @@ 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.evomaster.client.java.controller.redis.ReflectionBasedRedisClient; import org.junit.jupiter.api.*; import org.testcontainers.containers.GenericContainer; import org.testcontainers.utility.DockerImageName; @@ -15,7 +15,7 @@ class RedisHandlerIntegrationTest { private static final int REDIS_PORT = 6379; private GenericContainer redisContainer; - private RedisClient client; + private ReflectionBasedRedisClient client; private RedisHandler handler; private int port; @@ -27,7 +27,7 @@ void setupContainer() { port = redisContainer.getMappedPort(REDIS_PORT); - client = new RedisClient("localhost", port); + client = new ReflectionBasedRedisClient("localhost", port); } @BeforeEach @@ -35,7 +35,6 @@ void setupHandler() { handler = new RedisHandler(); handler.setRedisClient(client); handler.setCalculateHeuristics(true); - handler.setExtractRedisExecution(true); client.flushAll(); } 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 index 023adc89b4..475e45f465 100644 --- 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 @@ -28,7 +28,6 @@ void testHandleStoresCommands() { ); handler.handle(cmd); - assertTrue(handler.isExtractRedisExecution()); List evals = handler.getEvaluatedRedisCommands(); assertNotNull(evals); 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 index 26c3fa7a02..3ef2045220 100644 --- 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 @@ -93,8 +93,8 @@ void testHGetFieldExists() { ); List redisInfoList = new ArrayList<>(); - redisInfoList.add(new RedisInfo("profile", true)); - redisInfoList.add(new RedisInfo("users", false)); + redisInfoList.add(new RedisInfo("profile", Collections.singletonMap("name", "John"))); + redisInfoList.add(new RedisInfo("users", Collections.emptyMap())); RedisDistanceWithMetrics result = calculator.computeDistance(cmd, redisInfoList); @@ -113,7 +113,7 @@ void testHGetFieldNotExists() { ); List redisInfoList = new ArrayList<>(); - redisInfoList.add(new RedisInfo("profile", false)); + redisInfoList.add(new RedisInfo("profile", Collections.emptyMap())); RedisDistanceWithMetrics result = calculator.computeDistance(cmd, redisInfoList); @@ -121,6 +121,40 @@ void testHGetFieldNotExists() { assertTrue(result.getNumberOfEvaluatedKeys() >= 1); } + @Test + void testHGetFieldDistance() { + RedisCommand lowerDistanceCmd = new RedisCommand( + RedisCommand.RedisCommandType.HGET, + new String[]{"key", "key"}, + true, + 3 + ); + RedisCommand cmd = new RedisCommand( + RedisCommand.RedisCommandType.HGET, + new String[]{"key", "key"}, + true, + 3 + ); + RedisCommand greaterDistanceCmd = new RedisCommand( + RedisCommand.RedisCommandType.HGET, + new String[]{"key", "key"}, + true, + 3 + ); + + List redisInfoList = new ArrayList<>(); + redisInfoList.add(new RedisInfo("profile", Collections.singletonMap("height", "175"))); + + RedisDistanceWithMetrics resultLower = calculator.computeDistance(lowerDistanceCmd, redisInfoList); + RedisDistanceWithMetrics result = calculator.computeDistance(cmd, redisInfoList); + RedisDistanceWithMetrics resultGreater = calculator.computeDistance(greaterDistanceCmd, redisInfoList); + + assertTrue(resultLower.getDistance() < result.getDistance(), + "Closer target field should yield lower distance"); + assertTrue(result.getDistance() < resultGreater.getDistance(), + "Closer target key and field should yield lower distance"); + } + /** * Distance in intersection between two given sets: * - setA y setB share members → low distance 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 abe10e088e..f50ab6fae3 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 @@ -60,6 +60,13 @@ public enum RedisCommandType { * This operation is limited to 64-bit signed integers. * INCR Documentation */ + HSET("hset", "hash", false), + /** + * Sets the specified fields to their respective values in the hash stored at key. + * This command overwrites the values of specified fields that exist in the hash. + * If key doesn't exist, a new key holding a hash is created. + * HSET Documentation + */ INCR("incr", "string", false), /** * Returns all keys matching pattern. @@ -216,4 +223,8 @@ public boolean getSuccessfullyExecuted() { public long getExecutionTime() { return executionTime; } + + public String toString(){ + return this.getType().getLabel() + " " + String.join(" ", this.getArgs()); + } } diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/StatefulConnectionClassReplacement.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/StatefulConnectionClassReplacement.java index 7880dc10b8..8fe78e2be3 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/StatefulConnectionClassReplacement.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/StatefulConnectionClassReplacement.java @@ -72,13 +72,6 @@ public static Object dispatch(Object redis, @ThirdPartyCast(actualType = "io.let } } - private static String stripKey(String token) { - if (token != null && token.startsWith("key<") && token.endsWith(">")) { - return token.substring(4, token.length() - 1); - } - return token; - } - private static void addRedisCommand(RedisCommand.RedisCommandType type, String[] args, long executionTime) { RedisCommand cmd = new RedisCommand( type, diff --git a/core-tests/e2e-tests/spring/pom.xml b/core-tests/e2e-tests/spring/pom.xml index 808bca3e41..d18daf833c 100644 --- a/core-tests/e2e-tests/spring/pom.xml +++ b/core-tests/e2e-tests/spring/pom.xml @@ -27,6 +27,7 @@ spring-graphql-bb spring-rest-multidb spring-rest-opensearch + spring-rest-redis spring-rest-rsa spring-rpc-grpc spring-rpc-thrift diff --git a/core-tests/e2e-tests/spring/spring-rest-redis/pom.xml b/core-tests/e2e-tests/spring/spring-rest-redis/pom.xml new file mode 100644 index 0000000000..edd9c9e352 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-redis/pom.xml @@ -0,0 +1,103 @@ + + + 4.0.0 + + evomaster-e2e-tests-spring + org.evomaster + 5.0.1-SNAPSHOT + + + evomaster-e2e-tests-spring-rest-redis + jar + + + + org.evomaster + evomaster-e2e-tests-utils + test-jar + + + org.evomaster + evomaster-client-java-controller + + + org.evomaster + evomaster-core + test + + + org.evomaster + evomaster-client-java-instrumentation + test-jar + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.springframework.boot + spring-boot-starter-web + + + javax.validation + validation-api + 2.0.1.Final + + + org.springframework.boot + spring-boot-starter-data-redis + + + redis.clients + jedis + + + io.springfox + springfox-swagger2 + + + io.swagger + * + + + + + io.rest-assured + rest-assured + + + org.springframework.boot + spring-boot-starter-security + + + + + + + kotlin-maven-plugin + org.jetbrains.kotlin + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + false + + + + + + + \ No newline at end of file diff --git a/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/SwaggerConfiguration.java b/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/SwaggerConfiguration.java new file mode 100644 index 0000000000..f36a36c4db --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/SwaggerConfiguration.java @@ -0,0 +1,38 @@ +package com.redis; + +import org.springframework.context.annotation.Bean; +import org.springframework.security.core.Authentication; +import org.springframework.web.context.request.WebRequest; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; + +import static springfox.documentation.builders.PathSelectors.regex; + +public class SwaggerConfiguration { + + String path; + + public SwaggerConfiguration(String path) { + this.path = path; + } + + @Bean + public Docket docketApi() { + return new Docket(DocumentationType.SWAGGER_2) + .apiInfo(apiInfo()) + .select() + .paths(regex("/" + path + "/.*")) + .build() + .ignoredParameterTypes(WebRequest.class, Authentication.class); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("API") + .description("Some description") + .version("1.0") + .build(); + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/RedisLettuceApp.java b/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/RedisLettuceApp.java new file mode 100644 index 0000000000..428beea34b --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/RedisLettuceApp.java @@ -0,0 +1,20 @@ +package com.redis.lettuce; + +import com.redis.SwaggerConfiguration; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +@EnableSwagger2 +@SpringBootApplication(exclude = SecurityAutoConfiguration.class) +public class RedisLettuceApp extends SwaggerConfiguration { + public RedisLettuceApp() { + super("redislettuce"); + } + + public static void main(String[] args) { + SpringApplication.run(RedisLettuceApp.class, args); + } + +} diff --git a/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/RedisLettuceController.java b/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/RedisLettuceController.java new file mode 100644 index 0000000000..cf7756fad8 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/RedisLettuceController.java @@ -0,0 +1,53 @@ +package com.redis.lettuce; + +import io.lettuce.core.api.sync.RedisCommands; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.RedisClient; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +@RestController +@RequestMapping(path = "/redislettuce") +public class RedisLettuceController { + + private RedisClient redisClient; + private StatefulRedisConnection connection; + private RedisCommands sync; + + @PostConstruct + public void init() { + String redisHost = System.getProperty("spring.redis.host", "localhost"); + String redisPort = System.getProperty("spring.redis.port", "6379"); + String redisUri = "redis://" + redisHost + ":" + redisPort; + redisClient = RedisClient.create(redisUri); + connection = redisClient.connect(); + sync = connection.sync(); + } + + @PreDestroy + public void shutdown() { + connection.close(); + redisClient.shutdown(); + } + + @PostMapping("/string/{key}") + public ResponseEntity saveData(@PathVariable String key) { + sync.set(key, "value"); + return ResponseEntity.status(200).build(); + } + + @GetMapping("/findKey/{key}") + public ResponseEntity findKey(@PathVariable String key) { + String result = sync.get(key); + if (result != null) { + return ResponseEntity.status(200).build(); + } else { + return ResponseEntity.status(404).build(); + } + } +} + + diff --git a/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/com/foo/spring/rest/redis/RedisController.java b/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/com/foo/spring/rest/redis/RedisController.java new file mode 100644 index 0000000000..87f9562326 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/com/foo/spring/rest/redis/RedisController.java @@ -0,0 +1,118 @@ +package com.foo.spring.rest.redis; + +import org.evomaster.client.java.controller.EmbeddedSutController; +import org.evomaster.client.java.controller.api.dto.auth.AuthenticationDto; +import org.evomaster.client.java.controller.api.dto.SutInfoDto; +import org.evomaster.client.java.controller.redis.ReflectionBasedRedisClient; +import org.evomaster.client.java.sql.DbSpecification; +import org.evomaster.client.java.controller.problem.ProblemInfo; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.ConfigurableApplicationContext; +import org.testcontainers.containers.GenericContainer; +import org.evomaster.client.java.controller.problem.RestProblem; +import redis.clients.jedis.Jedis; + +import java.util.List; +import java.util.Map; + +public abstract class RedisController extends EmbeddedSutController { + + private static final int REDIS_DB_PORT = 6379; + private Jedis redisClient; + + private final GenericContainer redisContainer = new GenericContainer<>("redis:7.0") + .withExposedPorts(REDIS_DB_PORT); + private ConfigurableApplicationContext ctx; + + private final String databaseName; + + private final Class redisAppClass; + + private String host; + private int port; + + protected RedisController(String databaseName, Class redisAppClass) { + this.databaseName = databaseName; + this.redisAppClass = redisAppClass; + super.setControllerPort(0); + } + + @Override + public String startSut() { + redisContainer.start(); + + String host = redisContainer.getHost(); + int port = redisContainer.getMappedPort(REDIS_DB_PORT); + + System.setProperty("spring.redis.host", host); + System.setProperty("spring.redis.port", String.valueOf(port)); + this.host = host; + this.port = port; + + redisClient = new Jedis(host, port); + + SpringApplicationBuilder app = new SpringApplicationBuilder(redisAppClass); + + app.properties( + "--server.port=0", + "spring.data.redis.host=" + host, + "spring.data.redis.port=" + port + ); + + ctx = app.run(); + resetStateOfSUT(); + + return "http://localhost:" + getSutPort(); + } + + @Override + public void stopSut() { + redisContainer.stop(); + ctx.stop(); + ctx.close(); + } + + @Override + public void resetStateOfSUT() { + redisClient.flushDB(); + } + + @Override + public List getDbSpecifications() { + return null; + } + + @Override + public boolean isSutRunning() { + return ctx != null && ctx.isRunning(); + } + + @Override + public List getInfoForAuthentication() { + return null; + } + + @Override + public ProblemInfo getProblemInfo() { + return new RestProblem( + "http://localhost:" + getSutPort() + "/v2/api-docs", + null + ); + } + + @Override + public SutInfoDto.OutputFormat getPreferredOutputFormat() { + return null; + } + + protected int getSutPort() { + return (Integer) ((Map) ctx.getEnvironment() + .getPropertySources().get("server.ports").getSource()) + .get("local.server.port"); + } + + @Override + public ReflectionBasedRedisClient getRedisConnection() { + return new ReflectionBasedRedisClient(this.host, this.port); + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/com/foo/spring/rest/redis/lettuce/RedisLettuceAppController.java b/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/com/foo/spring/rest/redis/lettuce/RedisLettuceAppController.java new file mode 100644 index 0000000000..4e9134ce4f --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/com/foo/spring/rest/redis/lettuce/RedisLettuceAppController.java @@ -0,0 +1,15 @@ +package com.foo.spring.rest.redis.lettuce; + +import com.foo.spring.rest.redis.RedisController; +import com.redis.lettuce.RedisLettuceApp; + +public class RedisLettuceAppController extends RedisController { + public RedisLettuceAppController() { + super("lettuce", RedisLettuceApp.class); + } + + @Override + public String getPackagePrefixesToCover() { + return "com.redis.lettuce"; + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/org/evomaster/e2etests/spring/rest/redis/RedisControllerTest.java b/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/org/evomaster/e2etests/spring/rest/redis/RedisControllerTest.java new file mode 100644 index 0000000000..78dd8a04cb --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/org/evomaster/e2etests/spring/rest/redis/RedisControllerTest.java @@ -0,0 +1,15 @@ +package org.evomaster.e2etests.spring.rest.redis; + +import com.foo.spring.rest.redis.RedisController; +import com.foo.spring.rest.redis.lettuce.RedisLettuceAppController; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class RedisControllerTest { + @Test + public void testCanStartSut() { + RedisController controller = new RedisLettuceAppController(); + controller.startSut(); + assertTrue(controller.isSutRunning()); + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/org/evomaster/e2etests/spring/rest/redis/lettuce/RedisLettuceEMTest.java b/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/org/evomaster/e2etests/spring/rest/redis/lettuce/RedisLettuceEMTest.java new file mode 100644 index 0000000000..257467fd75 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/org/evomaster/e2etests/spring/rest/redis/lettuce/RedisLettuceEMTest.java @@ -0,0 +1,46 @@ +package org.evomaster.e2etests.spring.rest.redis.lettuce; + +import com.foo.spring.rest.redis.lettuce.RedisLettuceAppController; +import org.evomaster.core.EMConfig; +import org.evomaster.core.problem.rest.data.HttpVerb; +import org.evomaster.core.problem.rest.data.RestIndividual; +import org.evomaster.core.search.Solution; +import org.evomaster.e2etests.utils.RestTestBase; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class RedisLettuceEMTest extends RestTestBase { + + @BeforeAll + public static void initClass() throws Exception { + + EMConfig config = new EMConfig(); + config.setInstrumentMR_REDIS(true); + RestTestBase.initClass(new RedisLettuceAppController(), config); + } + + @Test + public void testRunEM() throws Throwable { + + runTestHandlingFlakyAndCompilation( + "RedisLettuceEM", + "org.foo.spring.rest.redis.RedisLettuceEM", + 1000, + true, + (args) -> { + setOption(args, "heuristicsForRedis", "true"); + setOption(args, "instrumentMR_REDIS", "true"); + + Solution solution = initAndRun(args); + + assertFalse(solution.getIndividuals().isEmpty()); + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/redislettuce/string/{key}", null); + assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/redislettuce/findKey/{key}", null); + assertHasAtLeastOne(solution, HttpVerb.GET, 404, "/redislettuce/findKey/{key}", null); + }, + 3); + + } +} diff --git a/core/src/main/kotlin/org/evomaster/core/EMConfig.kt b/core/src/main/kotlin/org/evomaster/core/EMConfig.kt index eeee3f5d54..9e1cd452a3 100644 --- a/core/src/main/kotlin/org/evomaster/core/EMConfig.kt +++ b/core/src/main/kotlin/org/evomaster/core/EMConfig.kt @@ -1634,6 +1634,10 @@ class EMConfig { @Cfg("Tracking of Mongo commands to improve test generation") var heuristicsForMongo = true + @Experimental + @Cfg("Tracking of Redis commands to improve test generation") + var heuristicsForRedis = false + @Cfg("Enable extracting SQL execution info") var extractSqlExecutionInfo = true @@ -1795,6 +1799,12 @@ class EMConfig { @Experimental var instrumentMR_OPENSEARCH = false + @Cfg("Execute instrumentation for method replace with category REDIS." + + " Note: this applies only for languages in which instrumentation is applied at runtime, like Java/Kotlin" + + " on the JVM.") + @Experimental + var instrumentMR_REDIS = false + @Cfg("Enable to expand the genotype of REST individuals based on runtime information missing from Swagger") var expandRestIndividuals = true @@ -2905,6 +2915,7 @@ class EMConfig { if (instrumentMR_NET) categories.add(ReplacementCategory.NET.toString()) if (instrumentMR_MONGO) categories.add(ReplacementCategory.MONGO.toString()) if (instrumentMR_OPENSEARCH) categories.add(ReplacementCategory.OPENSEARCH.toString()) + if (instrumentMR_REDIS) categories.add(ReplacementCategory.REDIS.toString()) return categories.joinToString(",") } diff --git a/core/src/main/kotlin/org/evomaster/core/problem/enterprise/service/EnterpriseFitness.kt b/core/src/main/kotlin/org/evomaster/core/problem/enterprise/service/EnterpriseFitness.kt index fdd590ce24..f66950d1bd 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/enterprise/service/EnterpriseFitness.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/enterprise/service/EnterpriseFitness.kt @@ -16,7 +16,6 @@ import org.evomaster.core.search.action.Action import org.evomaster.core.search.action.ActionResult import org.evomaster.core.search.FitnessValue import org.evomaster.core.search.Individual -import org.evomaster.core.search.TargetInfo import org.evomaster.core.search.gene.sql.SqlAutoIncrementGene import org.evomaster.core.search.gene.sql.SqlForeignKeyGene import org.evomaster.core.search.gene.sql.SqlPrimaryKeyGene @@ -330,6 +329,10 @@ abstract class EnterpriseFitness : FitnessFunction() where T : Individual } fv.aggregateMongoDatabaseData() } + + if (configuration.heuristicsForRedis) { + handleRedisHeuristics(dto, fv) + } } private fun handleSqlHeuristics( @@ -405,4 +408,37 @@ abstract class EnterpriseFitness : FitnessFunction() where T : Individual } } } + + private fun handleRedisHeuristics(dto: TestResultsDto, fv: FitnessValue) { + for (i in 0 until dto.extraHeuristics.size) { + + val extra = dto.extraHeuristics[i] + + extraHeuristicsLogger.writeHeuristics(extra.heuristics, i) + + val toMinimize = extra.heuristics + .filter { + it != null + && it.objective == ExtraHeuristicEntryDto.Objective.MINIMIZE_TO_ZERO + && it.type == ExtraHeuristicEntryDto.Type.REDIS + }.map { it.value } + .toList() + + if (toMinimize.isNotEmpty()) { + fv.setExtraToMinimize(i, toMinimize) + } + + extra.heuristics + .filterNotNull().forEach { + if (it.type == ExtraHeuristicEntryDto.Type.REDIS) { + statistics.reportNumberOfEvaluatedDocumentsForRedisHeuristic(it.numberOfEvaluatedRecords) + if (it.extraHeuristicEvaluationFailure) { + statistics.reportRedisHeuristicEvaluationFailure() + } else { + statistics.reportRedisHeuristicEvaluationSuccess() + } + } + } + } + } } diff --git a/core/src/main/kotlin/org/evomaster/core/search/service/Statistics.kt b/core/src/main/kotlin/org/evomaster/core/search/service/Statistics.kt index 575862b8e0..4eb1561b9d 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/service/Statistics.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/service/Statistics.kt @@ -83,6 +83,11 @@ class Statistics : SearchListener { private var mongoHeuristicEvaluationFailureCount = 0 private val mongoDocumentsAverageCalculator = IncrementalAverage() + // redis heuristic evaluation statistic + private var redisHeuristicEvaluationSuccessCount = 0 + private var redisHeuristicEvaluationFailureCount = 0 + private val redisDocumentsAverageCalculator = IncrementalAverage() + class Pair(val header: String, val element: String) @@ -169,6 +174,10 @@ class Statistics : SearchListener { mongoDocumentsAverageCalculator.addValue(numberOfEvaluatedDocuments) } + fun reportNumberOfEvaluatedDocumentsForRedisHeuristic(numberOfEvaluatedDocuments: Int) { + redisDocumentsAverageCalculator.addValue(numberOfEvaluatedDocuments) + } + fun reportSqlParsingFailures(numberOfParsingFailures: Int) { if (numberOfParsingFailures<0) { throw IllegalArgumentException("Invalid number of parsing failures: $numberOfParsingFailures") @@ -192,6 +201,14 @@ class Statistics : SearchListener { mongoHeuristicEvaluationFailureCount++ } + fun reportRedisHeuristicEvaluationSuccess() { + redisHeuristicEvaluationSuccessCount++ + } + + fun reportRedisHeuristicEvaluationFailure() { + redisHeuristicEvaluationFailureCount++ + } + fun getMongoHeuristicsEvaluationCount(): Int = mongoHeuristicEvaluationSuccessCount + mongoHeuristicEvaluationFailureCount fun getSqlHeuristicsEvaluationCount(): Int = sqlHeuristicEvaluationSuccessCount + sqlHeuristicEvaluationFailureCount @@ -200,6 +217,10 @@ class Statistics : SearchListener { fun averageNumberOfEvaluatedDocumentsForMongoHeuristics(): Double = mongoDocumentsAverageCalculator.mean + fun getRedisHeuristicsEvaluationCount(): Int = redisHeuristicEvaluationSuccessCount + redisHeuristicEvaluationFailureCount + + fun averageNumberOfEvaluatedDocumentsForRedisHeuristics(): Double = redisDocumentsAverageCalculator.mean + override fun newActionEvaluated() { if (snapshotThreshold <= 0) { //not collecting snapshot data diff --git a/docs/options.md b/docs/options.md index a3ead34bf7..0757a565bd 100644 --- a/docs/options.md +++ b/docs/options.md @@ -276,11 +276,13 @@ There are 3 types of options: |`externalServiceIP`| __String__. User provided external service IP. When EvoMaster mocks external services, mock server instances will run on local addresses starting from this provided address. Min value is 127.0.0.4. Lower values like 127.0.0.2 and 127.0.0.3 are reserved. *Constraints*: `regex (?!^0*127(\.0*0){2}\.0*[0123]$)^0*127(\.0*(25[0-5]\|2[0-4][0-9]\|1?[0-9]?[0-9])){3}$`. *Default value*: `127.0.0.4`.| |`externalServiceIPSelectionStrategy`| __Enum__. Specify a method to select the first external service spoof IP address. *Valid values*: `NONE, DEFAULT, USER, RANDOM`. *Default value*: `NONE`.| |`generateSqlDataWithDSE`| __Boolean__. Enable EvoMaster to generate SQL data with direct accesses to the database. Use Dynamic Symbolic Execution. *Default value*: `false`.| +|`heuristicsForRedis`| __Boolean__. Tracking of Redis commands to improve test generation. *Default value*: `false`.| |`heuristicsForSQLAdvanced`| __Boolean__. If using SQL heuristics, enable more advanced version. *Default value*: `false`.| |`httpOracles`| __Boolean__. Extra checks on HTTP properties in returned responses, used as automated oracles to detect faults. *Default value*: `false`.| |`initStructureMutationProbability`| __Double__. Probability of applying a mutation that can change the structure of test's initialization if it has. *Constraints*: `probability 0.0-1.0`. *Default value*: `0.0`.| |`instrumentMR_NET`| __Boolean__. Execute instrumentation for method replace with category NET. Note: this applies only for languages in which instrumentation is applied at runtime, like Java/Kotlin on the JVM. *Default value*: `false`.| |`instrumentMR_OPENSEARCH`| __Boolean__. Execute instrumentation for method replace with category OPENSEARCH. Note: this applies only for languages in which instrumentation is applied at runtime, like Java/Kotlin on the JVM. *Default value*: `false`.| +|`instrumentMR_REDIS`| __Boolean__. Execute instrumentation for method replace with category REDIS. Note: this applies only for languages in which instrumentation is applied at runtime, like Java/Kotlin on the JVM. *Default value*: `false`.| |`languageModelConnector`| __Boolean__. Enable language model connector. *Default value*: `false`.| |`languageModelConnectorNumberOfThreads`| __Int__. Number of threads for language model connector. No more threads than numbers of processors will be used. *Constraints*: `min=1.0`. *Default value*: `2`.| |`languageModelName`| __String__. Large-language model name as listed in Ollama. *Default value*: `llama3.2:latest`.| diff --git a/pom.xml b/pom.xml index 98c90432fe..253a5ba0c8 100644 --- a/pom.xml +++ b/pom.xml @@ -152,6 +152,7 @@ 1.0.0 2.6.6 6.1.4.RELEASE + 5.1.0 @@ -584,6 +585,13 @@ ${io.lettuce.core.version} + + + redis.clients + jedis + ${redis.clients.jedis.version} + + org.opensearch.client