From 5a5b12385deab9da82aceb36b26270c191623876 Mon Sep 17 00:00:00 2001 From: Alexander Szyrej Date: Tue, 19 May 2026 15:35:20 -0300 Subject: [PATCH 1/6] Added actions for hset (hash commands) and tests --- .../execution/RedisFailedCommand.java | 18 ++- .../operations/RedisInsertionDto.java | 6 + .../internal/db/redis/RedisHandler.java | 52 +++++-- .../redis/RedisCommandExecutor.java | 12 +- .../java/controller/redis/dsl/RedisDsl.java | 19 +++ .../redis/dsl/RedisSequenceDsl.java | 10 ++ .../RedisLettuceFindHashNoSaveApp.java | 20 +++ .../RedisLettuceFindHashNoSaveRest.java | 33 +++++ .../RedisLettuceFindKeyNoSaveRest.java | 2 +- .../RedisLettuceFindHashNoSaveController.java | 15 ++ .../RedisLettuceFindKeyNoSaveController.java | 2 +- .../RedisLettuceFindHashNoSaveEMTest.java | 49 ++++++ .../RedisLettuceFindKeyNoSaveEMTest.java | 2 +- .../org/evomaster/core/output/RedisWriter.kt | 36 +++-- .../api/service/ApiWsStructureMutator.kt | 2 +- .../org/evomaster/core/redis/RedisDbAction.kt | 36 +---- .../core/redis/RedisDbActionTransformer.kt | 19 ++- .../evomaster/core/redis/RedisHsetAction.kt | 31 ++++ .../core/redis/RedisInsertBuilder.kt | 80 ++++++++-- .../evomaster/core/redis/RedisSetAction.kt | 26 ++++ .../core/search/EvaluatedIndividual.kt | 33 ++++- .../evomaster/core/search/action/Action.kt | 8 +- .../impactinfocollection/ImpactUtils.kt | 4 +- .../ImpactsOfIndividual.kt | 6 +- .../evomaster/core/output/RedisWriterTest.kt | 93 ++++++------ .../evomaster/core/redis/RedisDbActionTest.kt | 139 ++++++++++++++---- .../redis/RedisDbActionTransformerTest.kt | 42 +----- .../core/redis/RedisInsertBuilderTest.kt | 134 +++++++++++++---- 28 files changed, 699 insertions(+), 230 deletions(-) create mode 100644 core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/findhashnosave/RedisLettuceFindHashNoSaveApp.java create mode 100644 core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/findhashnosave/RedisLettuceFindHashNoSaveRest.java create mode 100644 core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/com/foo/spring/rest/redis/lettuce/findhashnosave/RedisLettuceFindHashNoSaveController.java rename core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/com/foo/spring/rest/redis/lettuce/findkeynosave/{findkey => }/RedisLettuceFindKeyNoSaveController.java (87%) create mode 100644 core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/org/evomaster/e2etests/spring/rest/redis/lettuce/findhashnosave/RedisLettuceFindHashNoSaveEMTest.java create mode 100644 core/src/main/kotlin/org/evomaster/core/redis/RedisHsetAction.kt create mode 100644 core/src/main/kotlin/org/evomaster/core/redis/RedisSetAction.kt diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/execution/RedisFailedCommand.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/execution/RedisFailedCommand.java index 4d06f6ac96..dfcdc180c6 100644 --- a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/execution/RedisFailedCommand.java +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/execution/RedisFailedCommand.java @@ -12,20 +12,26 @@ public class RedisFailedCommand { public String command; /** - * Command type. Corresponds to a RedisCommandType dataType. + * Key involved. Could be null if the command does not have a key in the arguments. For example: KEYS (pattern). */ - public String type; + public String key; /** - * Key involved. Could be null if the command does not have a key in the arguments. For example: KEYS (pattern). + * Pattern involved. It'd only apply to commands with pattern like KEYS. */ - public String key; + public String pattern; + + /** + * Field involved. It'd only apply to hash commands with a field like HGET. + */ + public String field; public RedisFailedCommand() {} - public RedisFailedCommand(String command, String key, String type) { + public RedisFailedCommand(String command, String key, String pattern, String field) { this.command = command; this.key = key; - this.type = type; + this.pattern = pattern; + this.field = field; } } diff --git a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/RedisInsertionDto.java b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/RedisInsertionDto.java index 636500a0d3..590326fa61 100644 --- a/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/RedisInsertionDto.java +++ b/client-java/controller-api/src/main/java/org/evomaster/client/java/controller/api/dto/database/operations/RedisInsertionDto.java @@ -5,9 +5,15 @@ */ public class RedisInsertionDto { + /** The Redis command.*/ + public String command; + /** The Redis key.*/ public String key; + /** The field associated to the value. */ + public String field; + /** The serialized value to set for that key. */ public String value; } 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 b2329ea0ee..38e626545a 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 @@ -3,16 +3,14 @@ import org.evomaster.client.java.controller.api.dto.database.execution.RedisExecutionsDto; import org.evomaster.client.java.controller.api.dto.database.execution.RedisFailedCommand; import org.evomaster.client.java.controller.internal.TaintHandlerExecutionTracer; -import org.evomaster.client.java.controller.redis.RedisKeyValueStore; -import org.evomaster.client.java.controller.redis.ReflectionBasedRedisClient; -import org.evomaster.client.java.controller.redis.RedisHeuristicsCalculator; -import org.evomaster.client.java.controller.redis.RedisValueData; +import org.evomaster.client.java.controller.redis.*; 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; +import static org.evomaster.client.java.instrumentation.RedisCommand.RedisCommandType.*; /** * Class used to act upon Redis commands executed by the SUT @@ -106,18 +104,54 @@ public List getEvaluatedRedisCommands() { } private void registerFailedCommand(RedisCommand redisCommand, double distance) { + RedisCommand.RedisCommandType type = redisCommand.getType(); if (distance > 0 && - redisCommand.getType().equals(RedisCommand.RedisCommandType.GET)) { + type.equals(GET) || type.equals(KEYS) || type.equals(HGET) || type.equals(HGETALL)) { //For this first iteration we'll only work on GET commands. failedCommands.add(createFailedCommand(redisCommand)); } } private RedisFailedCommand createFailedCommand(RedisCommand redisCommand) { - return new RedisFailedCommand( - redisCommand.getType().getLabel().toUpperCase(), - redisCommand.extractArgs().get(0), - redisCommand.getType().getDataType()); + RedisCommand.RedisCommandType type = redisCommand.getType(); + List args = redisCommand.extractArgs(); + switch (type) { + case GET: + case HGETALL: { + if (args.isEmpty()) { + throw new IllegalArgumentException("Command " + type.getLabel() + " has invalid arguments."); + } + return new RedisFailedCommand( + type.getLabel().toUpperCase(), + args.get(0), + null, + null); + } + + case KEYS: { + if (args.isEmpty()) { + throw new IllegalArgumentException("Command KEYS has invalid arguments."); + } + return new RedisFailedCommand( + type.getLabel().toUpperCase(), + null, + RedisUtils.redisPatternToRegex(args.get(0)), + null); + } + + case HGET: { + if (args.size() < 2) { + throw new IllegalArgumentException("Command HGET has invalid arguments."); + } + return new RedisFailedCommand( + type.getLabel().toUpperCase(), + args.get(0), + null, + args.get(1)); + } + default: + throw new RuntimeException("Invalid command registering failed redis commands."); + } } private RedisDistanceWithMetrics computeDistance(RedisCommand redisCommand, ReflectionBasedRedisClient redisClient) { diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/RedisCommandExecutor.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/RedisCommandExecutor.java index b17c3d9603..d8d9c39411 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/RedisCommandExecutor.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/RedisCommandExecutor.java @@ -30,7 +30,17 @@ public static RedisInsertionResultsDto executeInsert( for (int i = 0; i < insertions.size(); i++) { RedisInsertionDto dto = insertions.get(i); try { - client.setValue(dto.key, dto.value); + switch (dto.command.toUpperCase()) { + case "SET": + client.setValue(dto.key, dto.value); + break; + case "HSET": + client.hashSet(dto.key, dto.field, dto.value); + break; + default: + throw new IllegalArgumentException( + "Unsupported Redis command: " + dto.command); + } results.set(i, true); } catch (Exception e) { throw new RuntimeException( diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/dsl/RedisDsl.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/dsl/RedisDsl.java index e0f002ee73..040f17f075 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/dsl/RedisDsl.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/dsl/RedisDsl.java @@ -46,12 +46,31 @@ public RedisStatementDsl set(String key, String value) { throw new IllegalArgumentException("Unspecified key"); } RedisInsertionDto dto = new RedisInsertionDto(); + dto.command = "SET"; dto.key = key; dto.value = value; list.add(dto); return this; } + @Override + public RedisStatementDsl hset(String key, String field, String value) { + checkDsl(); + if (key == null || key.isEmpty()) { + throw new IllegalArgumentException("Unspecified key"); + } + if (field == null || field.isEmpty()) { + throw new IllegalArgumentException("Unspecified field"); + } + RedisInsertionDto dto = new RedisInsertionDto(); + dto.command = "HSET"; + dto.key = key; + dto.field = field; + dto.value = value; + list.add(dto); + return this; + } + @Override public RedisSequenceDsl and() { return this; diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/dsl/RedisSequenceDsl.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/dsl/RedisSequenceDsl.java index 7416c766fd..5908fd6c63 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/dsl/RedisSequenceDsl.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/dsl/RedisSequenceDsl.java @@ -10,4 +10,14 @@ public interface RedisSequenceDsl { * @return a statement object on which the sequence can be continued or closed */ RedisStatementDsl set(String key, String value); + + /** + * An HSET operation on the Redis database. + * + * @param key the hash key + * @param field the field within the hash + * @param value the string value to store at that field + * @return a statement object on which the sequence can be continued or closed + */ + RedisStatementDsl hset(String key, String field, String value); } diff --git a/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/findhashnosave/RedisLettuceFindHashNoSaveApp.java b/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/findhashnosave/RedisLettuceFindHashNoSaveApp.java new file mode 100644 index 0000000000..e8f18cc90f --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/findhashnosave/RedisLettuceFindHashNoSaveApp.java @@ -0,0 +1,20 @@ +package com.redis.lettuce.findhashnosave; + +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 RedisLettuceFindHashNoSaveApp extends SwaggerConfiguration { + public RedisLettuceFindHashNoSaveApp() { + super("redislettucefindhashnosave"); + } + + public static void main(String[] args) { + SpringApplication.run(RedisLettuceFindHashNoSaveApp.class, args); + } + +} diff --git a/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/findhashnosave/RedisLettuceFindHashNoSaveRest.java b/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/findhashnosave/RedisLettuceFindHashNoSaveRest.java new file mode 100644 index 0000000000..66c47d93a3 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/findhashnosave/RedisLettuceFindHashNoSaveRest.java @@ -0,0 +1,33 @@ +package com.redis.lettuce.findhashnosave; + +import com.redis.lettuce.AbstractRedisLettuceRest; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping(path = "/redislettucefindhashnosave") +public class RedisLettuceFindHashNoSaveRest extends AbstractRedisLettuceRest { + + @GetMapping("/findHash") + public ResponseEntity findHashNoSave() { + String result = sync.hget("key-for-findhash", "field"); + if (result != null) { + return ResponseEntity.status(200).build(); + } else { + return ResponseEntity.status(404).build(); + } + } + + @GetMapping("/findHashAllFields") + public ResponseEntity findHashesNoSave() { + var result = sync.hgetall("another-key-for-findhashes"); + if (result != null && !result.isEmpty()) { + return ResponseEntity.status(200).build(); + } else { + return ResponseEntity.status(404).build(); + } + } + +} + + diff --git a/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/findkeynosave/findkey/RedisLettuceFindKeyNoSaveRest.java b/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/findkeynosave/findkey/RedisLettuceFindKeyNoSaveRest.java index 1e1c4acca6..4a4d8970ef 100644 --- a/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/findkeynosave/findkey/RedisLettuceFindKeyNoSaveRest.java +++ b/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/findkeynosave/findkey/RedisLettuceFindKeyNoSaveRest.java @@ -10,7 +10,7 @@ public class RedisLettuceFindKeyNoSaveRest extends AbstractRedisLettuceRest { @GetMapping("/findKey") public ResponseEntity findKey() { - String result = sync.get("here-goes-the-key"); + String result = sync.get("key-for-findkey"); if (result != null) { return ResponseEntity.status(200).build(); } else { diff --git a/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/com/foo/spring/rest/redis/lettuce/findhashnosave/RedisLettuceFindHashNoSaveController.java b/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/com/foo/spring/rest/redis/lettuce/findhashnosave/RedisLettuceFindHashNoSaveController.java new file mode 100644 index 0000000000..2a8935cc8b --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/com/foo/spring/rest/redis/lettuce/findhashnosave/RedisLettuceFindHashNoSaveController.java @@ -0,0 +1,15 @@ +package com.foo.spring.rest.redis.lettuce.findhashnosave; + +import com.foo.spring.rest.redis.RedisController; +import com.redis.lettuce.findhashnosave.RedisLettuceFindHashNoSaveApp; + +public class RedisLettuceFindHashNoSaveController extends RedisController { + public RedisLettuceFindHashNoSaveController() { + super("lettuce", RedisLettuceFindHashNoSaveApp.class); + } + + @Override + public String getPackagePrefixesToCover() { + return "com.redis.lettuce.findhashnosave"; + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/com/foo/spring/rest/redis/lettuce/findkeynosave/findkey/RedisLettuceFindKeyNoSaveController.java b/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/com/foo/spring/rest/redis/lettuce/findkeynosave/RedisLettuceFindKeyNoSaveController.java similarity index 87% rename from core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/com/foo/spring/rest/redis/lettuce/findkeynosave/findkey/RedisLettuceFindKeyNoSaveController.java rename to core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/com/foo/spring/rest/redis/lettuce/findkeynosave/RedisLettuceFindKeyNoSaveController.java index 53a35e5d64..68eca64f8d 100644 --- a/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/com/foo/spring/rest/redis/lettuce/findkeynosave/findkey/RedisLettuceFindKeyNoSaveController.java +++ b/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/com/foo/spring/rest/redis/lettuce/findkeynosave/RedisLettuceFindKeyNoSaveController.java @@ -1,4 +1,4 @@ -package com.foo.spring.rest.redis.lettuce.findkeynosave.findkey; +package com.foo.spring.rest.redis.lettuce.findkeynosave; import com.foo.spring.rest.redis.RedisController; import com.redis.lettuce.findkeynosave.findkey.RedisLettuceFindKeyNoSaveApp; diff --git a/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/org/evomaster/e2etests/spring/rest/redis/lettuce/findhashnosave/RedisLettuceFindHashNoSaveEMTest.java b/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/org/evomaster/e2etests/spring/rest/redis/lettuce/findhashnosave/RedisLettuceFindHashNoSaveEMTest.java new file mode 100644 index 0000000000..fdd4cedad0 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/org/evomaster/e2etests/spring/rest/redis/lettuce/findhashnosave/RedisLettuceFindHashNoSaveEMTest.java @@ -0,0 +1,49 @@ +package org.evomaster.e2etests.spring.rest.redis.lettuce.findhashnosave; + +import com.foo.spring.rest.redis.lettuce.findhashnosave.RedisLettuceFindHashNoSaveController; +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.assertFalse; + +public class RedisLettuceFindHashNoSaveEMTest extends RestTestBase { + + @BeforeAll + public static void initClass() throws Exception { + + EMConfig config = new EMConfig(); + config.setInstrumentMR_REDIS(true); + RestTestBase.initClass(new RedisLettuceFindHashNoSaveController(), config); + } + + @Test + public void testFindHashNoSaveEM() throws Throwable { + + runTestHandlingFlakyAndCompilation( + "RedisLettuceFindHashNoSaveEM", + "org.foo.spring.rest.redis.RedisLettuceFindHashNoSaveEM", + 1000, + true, + (args) -> { + setOption(args, "heuristicsForRedis", "true"); + setOption(args, "instrumentMR_REDIS", "true"); + setOption(args, "extractRedisExecutionInfo", "true"); + setOption(args, "generateRedisData", "true"); + + Solution solution = initAndRun(args); + + assertFalse(solution.getIndividuals().isEmpty()); + assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/redislettucefindhashnosave/findHash", null); + assertHasAtLeastOne(solution, HttpVerb.GET, 404, "/redislettucefindhashnosave/findHash", null); + assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/redislettucefindhashnosave/findHashAllFields", null); + assertHasAtLeastOne(solution, HttpVerb.GET, 404, "/redislettucefindhashnosave/findHashAllFields", null); + }, + 3); + + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/org/evomaster/e2etests/spring/rest/redis/lettuce/findkeynosave/RedisLettuceFindKeyNoSaveEMTest.java b/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/org/evomaster/e2etests/spring/rest/redis/lettuce/findkeynosave/RedisLettuceFindKeyNoSaveEMTest.java index 76cae1dad7..f5b19b65f2 100644 --- a/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/org/evomaster/e2etests/spring/rest/redis/lettuce/findkeynosave/RedisLettuceFindKeyNoSaveEMTest.java +++ b/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/org/evomaster/e2etests/spring/rest/redis/lettuce/findkeynosave/RedisLettuceFindKeyNoSaveEMTest.java @@ -1,6 +1,6 @@ package org.evomaster.e2etests.spring.rest.redis.lettuce.findkeynosave; -import com.foo.spring.rest.redis.lettuce.findkeynosave.findkey.RedisLettuceFindKeyNoSaveController; +import com.foo.spring.rest.redis.lettuce.findkeynosave.RedisLettuceFindKeyNoSaveController; import org.evomaster.core.EMConfig; import org.evomaster.core.problem.rest.data.HttpVerb; import org.evomaster.core.problem.rest.data.RestIndividual; diff --git a/core/src/main/kotlin/org/evomaster/core/output/RedisWriter.kt b/core/src/main/kotlin/org/evomaster/core/output/RedisWriter.kt index 9794bee05b..a6ff7f5bd0 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/RedisWriter.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/RedisWriter.kt @@ -1,6 +1,7 @@ package org.evomaster.core.output import org.apache.commons.lang3.StringEscapeUtils +import org.evomaster.core.redis.* import org.evomaster.core.search.action.EvaluatedRedisDbAction /** @@ -40,15 +41,22 @@ object RedisWriter { redisDbInitialization .filter { !skipFailure || it.redisResult.getInsertExecutionResult() } - .forEachIndexed { index, evaluatedRedisDbAction -> + .forEachIndexed { index, evaluated -> - val escapedKey = StringEscapeUtils.escapeJava( - evaluatedRedisDbAction.redisAction.key - ) - val rawValue = evaluatedRedisDbAction.redisAction.valueGene.getValueAsRawString() - val escapedValue = StringEscapeUtils.escapeJava(rawValue).let { escaped -> - // In Kotlin generated tests, dollar signs in string literals must be escaped - if (format.isKotlin()) escaped.replace("$", "\\$") else escaped + val action = evaluated.redisAction + + val dslCall = when (action) { + is RedisSetAction -> { + val key = escape(action.keyGene.getValueAsRawString(), format) + val value = escape(action.valueGene.getValueAsRawString(), format) + ".set(\"$key\", \"$value\")" + } + is RedisHsetAction -> { + val key = escape(action.keyGene.getValueAsRawString(), format) + val field = escape(action.field, format) + val value = escape(action.valueGene.getValueAsRawString(), format) + ".hset(\"$key\", \"$field\", \"$value\")" + } } lines.add( @@ -58,12 +66,10 @@ object RedisWriter { index == 0 && format.isKotlin() -> "val $insertionVar = redis($previousVar)" else -> ".and()" - } + ".set(\"$escapedKey\", \"$escapedValue\")" + } + dslCall ) - if (index == 0) { - lines.indent() - } + if (index == 0) lines.indent() } lines.add(".dtos()") @@ -83,4 +89,10 @@ object RedisWriter { insertionVars.add(insertionVar to insertionVarResult) } + + private fun escape(value: String, format: OutputFormat): String { + return StringEscapeUtils.escapeJava(value).let { + if (format.isKotlin()) it.replace("$", "\\$") else it + } + } } diff --git a/core/src/main/kotlin/org/evomaster/core/problem/api/service/ApiWsStructureMutator.kt b/core/src/main/kotlin/org/evomaster/core/problem/api/service/ApiWsStructureMutator.kt index 63c436151b..8de9d5ee6a 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/api/service/ApiWsStructureMutator.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/api/service/ApiWsStructureMutator.kt @@ -474,7 +474,7 @@ abstract class ApiWsStructureMutator : StructureMutator() { val existingKeys = ind.seeInitializingActions() .filterIsInstance() - .map { it.key } + .mapNotNull { it.getTargetKey() } .toSet() val addedActions = RedisInsertBuilder.buildInsertActions( diff --git a/core/src/main/kotlin/org/evomaster/core/redis/RedisDbAction.kt b/core/src/main/kotlin/org/evomaster/core/redis/RedisDbAction.kt index f89c8f36bc..451c5dd302 100644 --- a/core/src/main/kotlin/org/evomaster/core/redis/RedisDbAction.kt +++ b/core/src/main/kotlin/org/evomaster/core/redis/RedisDbAction.kt @@ -1,43 +1,19 @@ package org.evomaster.core.redis import org.evomaster.core.search.action.EnvironmentAction -import org.evomaster.core.search.action.Action -import org.evomaster.core.search.gene.Gene -import org.evomaster.core.search.gene.string.StringGene /** * Represents an action to insert data into a Redis database, generated in response * to a failed Redis read command. */ -class RedisDbAction( +sealed class RedisDbAction : EnvironmentAction(listOf()) { /** - * Immutable key for which Redis returned no data. + * Returns the primary key this action targets, if any. + * Commands that operate on patterns rather than specific keys (e.g., KEYS) + * return null. */ - val key: String, - /** - * Value associated to the key. - */ - val valueGene: StringGene, - /** - * Command type executed. - */ - val dataType: RedisDataType -) : EnvironmentAction(listOf()) { - - enum class RedisDataType { - STRING - } - - override fun seeTopGenes(): List = listOf(valueGene) - - override fun copyContent(): Action { - return RedisDbAction( - key, - valueGene.copy() as StringGene, - dataType - ) - } + open fun getTargetKey(): String? = null - override fun getName(): String = "Redis_${dataType}_${key}" + override fun getActionGroupKey(): String = RedisDbAction::class.java.name } \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/redis/RedisDbActionTransformer.kt b/core/src/main/kotlin/org/evomaster/core/redis/RedisDbActionTransformer.kt index cd8bded6bf..043e4f7991 100644 --- a/core/src/main/kotlin/org/evomaster/core/redis/RedisDbActionTransformer.kt +++ b/core/src/main/kotlin/org/evomaster/core/redis/RedisDbActionTransformer.kt @@ -8,13 +8,20 @@ import org.evomaster.client.java.controller.api.dto.database.operations.* object RedisDbActionTransformer { fun transform(actions: List): RedisDatabaseCommandsDto { - val dto = - RedisDatabaseCommandsDto() + val dto = RedisDatabaseCommandsDto() dto.insertions = actions.map { action -> - // Current version supports only GET to SET commands. Command keyword is not included at the moment. - RedisInsertionDto().also { - it.key = action.key - it.value = action.valueGene.value + when (action) { + is RedisSetAction -> RedisInsertionDto().also { + it.command = "SET" + it.key = action.keyGene.value + it.value = action.valueGene.value + } + is RedisHsetAction -> RedisInsertionDto().also { + it.command = "HSET" + it.key = action.keyGene.value + it.field = action.field + it.value = action.valueGene.value + } } } return dto diff --git a/core/src/main/kotlin/org/evomaster/core/redis/RedisHsetAction.kt b/core/src/main/kotlin/org/evomaster/core/redis/RedisHsetAction.kt new file mode 100644 index 0000000000..1ce231ae01 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/redis/RedisHsetAction.kt @@ -0,0 +1,31 @@ +package org.evomaster.core.redis + +import org.evomaster.core.search.action.Action +import org.evomaster.core.search.gene.Gene +import org.evomaster.core.search.gene.string.StringGene + +/** + * Represents an HSET action, generated from a failed HGET or HGETALL command. + * + *

For HGET, [field] is the exact field observed in the failed command. + * For HGETALL, [field] is a placeholder since the expected fields are unknown. + * + * @param keyGene the new key to be inserted. + * @param field the hash field to insert. + * @param valueGene gene representing the field value to insert. + */ +class RedisHsetAction( + val keyGene: StringGene, + val field: String, + val valueGene: StringGene +) : RedisDbAction() { + + override fun getTargetKey() = keyGene.value + + override fun seeTopGenes(): List = listOf(valueGene) + + override fun copyContent(): Action = + RedisHsetAction(keyGene, field, valueGene.copy() as StringGene) + + override fun getName() = "Redis_HSET_${keyGene}_$field" +} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/redis/RedisInsertBuilder.kt b/core/src/main/kotlin/org/evomaster/core/redis/RedisInsertBuilder.kt index b2001022c2..26ce842dad 100644 --- a/core/src/main/kotlin/org/evomaster/core/redis/RedisInsertBuilder.kt +++ b/core/src/main/kotlin/org/evomaster/core/redis/RedisInsertBuilder.kt @@ -1,13 +1,19 @@ package org.evomaster.core.redis import org.evomaster.client.java.controller.api.dto.database.execution.RedisFailedCommand +import org.evomaster.client.java.instrumentation.shared.StringSpecialization +import org.evomaster.client.java.instrumentation.shared.StringSpecializationInfo import org.evomaster.core.search.gene.string.StringGene import org.evomaster.core.search.service.Randomness /** - * Transforms a failed Redis command in an insert action. + * Transforms a failed Redis read command into the corresponding insert action. * - * Example: GET key -> RedisDbAction(key, StringGene, RedisDataType STRING) + * Each supported command type maps to a specific [RedisDbAction] subclass: + * - GET key -> [RedisSetAction] + * - KEYS pattern -> [RedisSetAction] + * - HGET key field -> [RedisHsetAction] + * - HGETALL key -> [RedisHsetAction] */ object RedisInsertBuilder { @@ -18,18 +24,64 @@ object RedisInsertBuilder { ): List { return failedCommands - .filter { it.key !in existingKeys } - .map { cmd -> - // Only GET commands with a StringGene as value is supported at the moment. - // More complex types will be included in the future. - val valueGene = StringGene("value").also { - it.randomize(randomness, false) + .filter { it.key == null || it.key !in existingKeys } + .mapNotNull { cmd -> + when (cmd.command) { + "GET" -> { + val keyGene = StringGene("key", cmd.key) + val valueGene = StringGene("value").also { + it.randomize(randomness, false) + } + RedisSetAction( + keyGene = keyGene, + valueGene = valueGene + ) + } + "HGET" -> { + val keyGene = StringGene("key", cmd.key) + val valueGene = StringGene("value").also { + it.randomize(randomness, false) + } + RedisHsetAction( + keyGene = keyGene, + field = cmd.field, + valueGene = valueGene + ) + } + "HGETALL" -> { + val keyGene = StringGene("key", cmd.key) + val valueGene = StringGene("value").also { + it.randomize(randomness, false) + } + // field is unknown — using a placeholder so the key exists in Redis + RedisHsetAction( + keyGene = keyGene, + field = "field", + valueGene = valueGene + ) + } + "KEYS" -> { + val keyGene = StringGene("key").also { + it.addSpecializations("key", + listOf(StringSpecializationInfo(StringSpecialization.REGEX_WHOLE, cmd.pattern)), + randomness, + updateGlobalInfo = false, // should this be false? + enableConstraintHandling = false) + it.doInitialize(randomness) + } + val valueGene = StringGene("value").also { + it.randomize(randomness, false) + } + RedisSetAction( + keyGene = keyGene, + valueGene = valueGene + ) + } + else -> { + // unsupported command + null + } } - RedisDbAction( - key = cmd.key, - valueGene = valueGene, - dataType = RedisDbAction.RedisDataType.STRING - ) } } -} +} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/redis/RedisSetAction.kt b/core/src/main/kotlin/org/evomaster/core/redis/RedisSetAction.kt new file mode 100644 index 0000000000..fe76efacdd --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/redis/RedisSetAction.kt @@ -0,0 +1,26 @@ +package org.evomaster.core.redis + +import org.evomaster.core.search.action.Action +import org.evomaster.core.search.gene.Gene +import org.evomaster.core.search.gene.string.StringGene + +/** + * Represents a SET action, generated from a failed GET command. + * + * @param keyGene the new key to be inserted + * @param valueGene gene representing the string value to insert + */ +class RedisSetAction( + val keyGene: StringGene, + val valueGene: StringGene +) : RedisDbAction() { + + override fun getTargetKey() = keyGene.value + + override fun seeTopGenes(): List = listOf(valueGene) + + override fun copyContent(): Action = + RedisSetAction(keyGene, valueGene.copy() as StringGene) + + override fun getName() = "Redis_SET_$keyGene" +} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/search/EvaluatedIndividual.kt b/core/src/main/kotlin/org/evomaster/core/search/EvaluatedIndividual.kt index 11468f29f6..d597b9bb32 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/EvaluatedIndividual.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/EvaluatedIndividual.kt @@ -110,15 +110,26 @@ class EvaluatedIndividual( trackOperator = trackOperator, index = index, impactInfo = if ((config.isEnabledImpactCollection())) { - val initActionTypes = individual.seeInitializingActions().groupBy { it::class }.keys.toList() + val initActionTypes = individual.seeInitializingActions() + .map { Class.forName(it.getActionGroupKey()).kotlin } + .distinct() if (individual is RestIndividual && config.isEnabledResourceDependency()) ResourceImpactOfIndividual(individual, initActionTypes, config.abstractInitializationGeneToMutate, fitness) else - ImpactsOfIndividual(individual, initActionTypes , config.abstractInitializationGeneToMutate, fitness) + ImpactsOfIndividual(individual, initActionTypes, config.abstractInitializationGeneToMutate, fitness) } else null ) + /** + * Returns the impact group key for this action, used to look up entries in [ImpactsOfIndividual.initActionImpacts]. + * For [EnvironmentAction] subclasses, this delegates to [EnvironmentAction.getActionGroupKey], which allows + * abstract base classes (e.g., RedisDbAction) to group all their concrete subtypes under a single key. + * For all other actions, falls back to the actual class name. + */ + private fun Action.getInitActionClassName(): String = + if (this is EnvironmentAction) this.getActionGroupKey() else this::class.java.name + fun copy(): EvaluatedIndividual { val ei = EvaluatedIndividual( @@ -609,7 +620,9 @@ class EvaluatedIndividual( } /** - * sync impact info based on [mutated] + * sync impact info based on [mutated]. + * Uses [Action.getInitActionClassName] to resolve the impact group key, so that + * abstract base classes (e.g., RedisDbAction) correctly map all their concrete subtypes. */ private fun syncImpact(previous: Individual, mutated: Individual) { // db action @@ -625,7 +638,7 @@ class EvaluatedIndividual( if (p != null){ impactInfo!!.getGene( actionName = action.getName(), - initActionClassName = action::class.java.name, + initActionClassName = action.getInitActionClassName(), geneId = rootGeneId, actionIndex = index, localId = action.getLocalId(), @@ -751,7 +764,7 @@ class EvaluatedIndividual( action as Action return impactInfo.getGene( actionName = action!!.getName(), - initActionClassName = action!!::class.java.name, + initActionClassName = action!!.getInitActionClassName(), geneId = id, actionIndex = actionList.indexOf(action), localId = null, @@ -920,14 +933,17 @@ class EvaluatedIndividual( impactInfo ?: return null if(fromInitialization){ val initAction = individual.seeInitializingActions()[actionIndex] - val relativeIndex = individual.seeInitializingActions().filter { it::class.java.name == initAction::class.java.name }.indexOf(initAction) + val impactKey = initAction.getInitActionClassName() + val relativeIndex = individual.seeInitializingActions() + .filter { it.getInitActionClassName() == impactKey } + .indexOf(initAction) return impactInfo.findImpactsByAction( actionName = initAction.getName(), actionIndex = relativeIndex, localId = null, fixedIndexedAction = true, fromInitialization = fromInitialization, - initActionClass = initAction::class.java.name + initActionClass = impactKey ) } @@ -989,9 +1005,10 @@ class EvaluatedIndividual( } return !invalid } + private fun initializingActionClasses(): List> { return listOf(MongoDbAction::class, SqlAction::class, RedisDbAction::class, ScheduleTaskAction::class) } fun hasAnyPotentialFault() = this.fitness.hasAnyPotentialFault(this.individual.searchGlobalState!!.idMapper) -} +} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/search/action/Action.kt b/core/src/main/kotlin/org/evomaster/core/search/action/Action.kt index 4b48a44e64..48243d6cdb 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/action/Action.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/action/Action.kt @@ -3,7 +3,6 @@ package org.evomaster.core.search.action import org.evomaster.core.search.Individual import org.evomaster.core.search.StructuralElement import org.evomaster.core.search.gene.Gene -import org.evomaster.core.search.gene.interfaces.TaintableGene import org.evomaster.core.search.service.Randomness import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -117,4 +116,11 @@ abstract class Action(children: List) : ActionComponent( * once an action is mounted inside an initialized individual */ open fun resolveTempData() : Boolean = true + + /** + * Returns the key used to group this action in the impact collection system. + * By default, returns the actual class name. Subclasses can override this to + * group multiple concrete types under a single logical key. + */ + open fun getActionGroupKey(): String = this::class.java.name } diff --git a/core/src/main/kotlin/org/evomaster/core/search/impact/impactinfocollection/ImpactUtils.kt b/core/src/main/kotlin/org/evomaster/core/search/impact/impactinfocollection/ImpactUtils.kt index d562c8074f..e574ba264f 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/impact/impactinfocollection/ImpactUtils.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/impact/impactinfocollection/ImpactUtils.kt @@ -170,7 +170,7 @@ class ImpactUtils { action is ApiExternalServiceAction, previous, mutatedGenes.size, - actionTypeClass = action::class.java.name + actionTypeClass = action.getActionGroupKey() )) } } @@ -224,7 +224,7 @@ class ImpactUtils { isDynamicAction = index == null, previous = previous, numOfMutatedGene = num, - actionTypeClass = a::class.java.name + actionTypeClass = a.getActionGroupKey() )) } } diff --git a/core/src/main/kotlin/org/evomaster/core/search/impact/impactinfocollection/ImpactsOfIndividual.kt b/core/src/main/kotlin/org/evomaster/core/search/impact/impactinfocollection/ImpactsOfIndividual.kt index f45a022b72..1d578c74e3 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/impact/impactinfocollection/ImpactsOfIndividual.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/impact/impactinfocollection/ImpactsOfIndividual.kt @@ -238,9 +238,9 @@ open class ImpactsOfIndividual( */ fun syncBasedOnIndividual(individual: Individual, initializingActionClasses: List>?) { individual.seeInitializingActions() - .filter { initializingActionClasses == null || initializingActionClasses.any { k->k.isInstance(it) } }.groupBy { it::class.java.name }.forEach {g-> - syncInitActionsBasedOnIndividual(individual, g.key, g.value) - } + .filter { initializingActionClasses == null || initializingActionClasses.any { k->k.isInstance(it) } } + .groupBy { it.getActionGroupKey() } + .forEach { g -> syncInitActionsBasedOnIndividual(individual, g.key, g.value) } //for fixed action val fixed = individual.seeFixedMainActions() diff --git a/core/src/test/kotlin/org/evomaster/core/output/RedisWriterTest.kt b/core/src/test/kotlin/org/evomaster/core/output/RedisWriterTest.kt index 96fff0d6f6..5ce8336ca1 100644 --- a/core/src/test/kotlin/org/evomaster/core/output/RedisWriterTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/output/RedisWriterTest.kt @@ -1,24 +1,23 @@ package org.evomaster.core.output -import org.evomaster.core.redis.RedisDbAction import org.evomaster.core.redis.RedisDbActionResult +import org.evomaster.core.redis.RedisHsetAction +import org.evomaster.core.redis.RedisSetAction import org.evomaster.core.search.action.EvaluatedRedisDbAction - import org.evomaster.core.search.gene.string.StringGene import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test class RedisWriterTest { - private fun makeEvaluated( + private fun makeEvaluatedSet( key: String, value: String, success: Boolean = true ): EvaluatedRedisDbAction { - val action = RedisDbAction( - key = key, - valueGene = StringGene("value", value), - dataType = RedisDbAction.RedisDataType.STRING + val action = RedisSetAction( + keyGene = StringGene("key", key), + valueGene = StringGene("value", value) ) action.setLocalId("test-redis-action") val result = RedisDbActionResult(action.getLocalId()).also { @@ -27,6 +26,24 @@ class RedisWriterTest { return EvaluatedRedisDbAction(action, result) } + private fun makeEvaluatedHset( + key: String, + field: String, + value: String, + success: Boolean = true + ): EvaluatedRedisDbAction { + val action = RedisHsetAction( + keyGene = StringGene("key", key), + field = field, + valueGene = StringGene("value", value) + ) + action.setLocalId("test-redis-hset-action") + val result = RedisDbActionResult(action.getLocalId()).also { + it.setInsertExecutionResult(success) + } + return EvaluatedRedisDbAction(action, result) + } + private fun writeKotlin( actions: List, insertionVars: MutableList> = mutableListOf(), @@ -68,49 +85,27 @@ class RedisWriterTest { } @Test - fun testSkipFailureFiltersFailedInsertions() { - val actions = listOf( - makeEvaluated("key:1", "val1", success = true), - makeEvaluated("key:2", "val2", success = false) - ) - - val output = writeKotlin(actions, skipFailure = true) - - assertTrue(output.contains("key:1")) - assertFalse(output.contains("key:2")) - } - - @Test - fun testSkipFailureAllFailedGeneratesNothing() { - val actions = listOf(makeEvaluated("key:1", "val1", success = false)) - - val output = writeKotlin(actions, skipFailure = true) - - assertTrue(output.isBlank()) - } - - @Test - fun testKotlinFormatSingleAction() { - val output = writeKotlin(listOf(makeEvaluated("user:1", "Alice"))) + fun testKotlinFormatSingleHsetAction() { + val output = writeKotlin(listOf(makeEvaluatedHset("user:1", "name", "Alice"))) assertTrue(output.contains("val insertions_redis = redis()")) - assertTrue(output.contains(".set(\"user:1\", \"Alice\")")) + assertTrue(output.contains(".hset(\"user:1\", \"name\", \"Alice\")")) assertTrue(output.contains(".dtos()")) assertTrue(output.contains("val insertions_redis_result = controller.execInsertionsIntoRedisDatabase(insertions_redis)")) } @Test - fun testJavaFormatSingleAction() { - val output = writeJava(listOf(makeEvaluated("user:1", "Alice"))) + fun testJavaFormatSingleHsetAction() { + val output = writeJava(listOf(makeEvaluatedHset("user:1", "name", "Alice"))) assertTrue(output.contains("List insertions_redis = redis()")) - assertTrue(output.contains(".set(\"user:1\", \"Alice\")")) + assertTrue(output.contains(".hset(\"user:1\", \"name\", \"Alice\")")) assertTrue(output.contains("RedisInsertionResultsDto insertions_redis_result = controller.execInsertionsIntoRedisDatabase(insertions_redis)")) } @Test fun testKotlinEscapesDollarSign() { - val output = writeKotlin(listOf(makeEvaluated("key", "\$HOME"))) + val output = writeKotlin(listOf(makeEvaluatedSet("key", "\$HOME"))) assertTrue(output.contains("\\${'$'}HOME") || output.contains("\\\$HOME")) assertFalse(output.contains("\"\$HOME\"")) @@ -118,7 +113,7 @@ class RedisWriterTest { @Test fun testKeyWithSpecialCharactersIsEscaped() { - val output = writeKotlin(listOf(makeEvaluated("key\"with\"quotes", "value"))) + val output = writeKotlin(listOf(makeEvaluatedSet("key\"with\"quotes", "value"))) assertTrue(output.contains("key\\\"with\\\"quotes")) } @@ -126,7 +121,7 @@ class RedisWriterTest { @Test fun testGroupIndexAppearsInVariableName() { val output = writeKotlin( - actions = listOf(makeEvaluated("key:1", "val")), + actions = listOf(makeEvaluatedSet("key:1", "val")), groupIndex = "_42" ) @@ -136,7 +131,7 @@ class RedisWriterTest { @Test fun testInsertionVarsAccumulated() { val insertionVars = mutableListOf>() - writeKotlin(listOf(makeEvaluated("key:1", "val")), insertionVars = insertionVars) + writeKotlin(listOf(makeEvaluatedSet("key:1", "val")), insertionVars = insertionVars) assertEquals(1, insertionVars.size) assertEquals("insertions_redis", insertionVars[0].first) @@ -144,10 +139,10 @@ class RedisWriterTest { } @Test - fun testMultipleActionsUsesAndChaining() { + fun testMultipleSetActionsUsesAndChaining() { val actions = listOf( - makeEvaluated("key:1", "val1"), - makeEvaluated("key:2", "val2") + makeEvaluatedSet("key:1", "val1"), + makeEvaluatedSet("key:2", "val2") ) val output = writeKotlin(actions) @@ -156,4 +151,18 @@ class RedisWriterTest { assertTrue(output.contains("key:1")) assertTrue(output.contains("key:2")) } + + @Test + fun testMixedSetAndHsetActions() { + val actions = listOf( + makeEvaluatedSet("string:key", "val"), + makeEvaluatedHset("hash:key", "field1", "val2") + ) + + val output = writeKotlin(actions) + + assertTrue(output.contains(".set(\"string:key\", \"val\")")) + assertTrue(output.contains(".and()")) + assertTrue(output.contains(".hset(\"hash:key\", \"field1\", \"val2\")")) + } } \ No newline at end of file diff --git a/core/src/test/kotlin/org/evomaster/core/redis/RedisDbActionTest.kt b/core/src/test/kotlin/org/evomaster/core/redis/RedisDbActionTest.kt index 5088cd4868..744c4181eb 100644 --- a/core/src/test/kotlin/org/evomaster/core/redis/RedisDbActionTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/redis/RedisDbActionTest.kt @@ -6,59 +6,142 @@ import org.junit.jupiter.api.Test class RedisDbActionTest { + // --- RedisSetAction --- + @Test - fun testSeeTopGenesReturnsValueGene() { - val action = RedisDbAction( - key = "user:1", - valueGene = StringGene("value"), - dataType = RedisDbAction.RedisDataType.STRING + fun testSetActionSeeTopGenesReturnsBothGenes() { + val action = RedisSetAction( + keyGene = StringGene("key", "user:1"), + valueGene = StringGene("value", "Alice") ) val genes = action.seeTopGenes() - assertEquals(1, genes.size) - assert(StringGene::class.isInstance(genes.first())) + assertEquals(2, genes.size) + assert(StringGene::class.isInstance(genes[0])) + assert(StringGene::class.isInstance(genes[1])) } @Test - fun testGetName() { - val action = RedisDbAction( - key = "user:1", - valueGene = StringGene("value"), - dataType = RedisDbAction.RedisDataType.STRING + fun testSetActionGetName() { + val action = RedisSetAction( + keyGene = StringGene("key", "user:1"), + valueGene = StringGene("value") ) - assertEquals("Redis_STRING_user:1", action.getName()) + assertEquals("Redis_SET_user:1", action.getName()) } @Test - fun testCopyContentPreservesFields() { - val original = RedisDbAction( - key = "session:abc", - valueGene = StringGene("value", "someData"), - dataType = RedisDbAction.RedisDataType.STRING + fun testSetActionCopyPreservesFields() { + val original = RedisSetAction( + keyGene = StringGene("key", "session:abc"), + valueGene = StringGene("value", "someData") ) - val copy = original.copy() as RedisDbAction + val copy = original.copy() as RedisSetAction - assertEquals(original.key, copy.key) - assertEquals(original.dataType, copy.dataType) + assertEquals(original.keyGene.value, copy.keyGene.value) assertEquals(original.valueGene.value, copy.valueGene.value) } @Test - fun testCopyContentIsIndependent() { - val original = RedisDbAction( - key = "product:1", - valueGene = StringGene("value", "original"), - dataType = RedisDbAction.RedisDataType.STRING + fun testSetActionCopyIsIndependent() { + val original = RedisSetAction( + keyGene = StringGene("key", "product:1"), + valueGene = StringGene("value", "original") ) - val copy = original.copy() as RedisDbAction + val copy = original.copy() as RedisSetAction copy.valueGene.value = "modified" - // mutating the copy must not affect the original assertEquals("original", original.valueGene.value) assertEquals("modified", copy.valueGene.value) } + @Test + fun testSetActionGetTargetKey() { + val action = RedisSetAction( + keyGene = StringGene("key", "user:1"), + valueGene = StringGene("value") + ) + + assertEquals("user:1", action.getTargetKey()) + } + + // --- RedisHsetAction --- + + @Test + fun testHsetActionSeeTopGenesReturnsBothGenes() { + val action = RedisHsetAction( + keyGene = StringGene("key", "user:1"), + field = "name", + valueGene = StringGene("value", "Alice") + ) + + val genes = action.seeTopGenes() + assertEquals(2, genes.size) + } + + @Test + fun testHsetActionGetName() { + val action = RedisHsetAction( + keyGene = StringGene("key", "user:1"), + field = "name", + valueGene = StringGene("value") + ) + + assertEquals("Redis_HSET_user:1_name", action.getName()) + } + + @Test + fun testHsetActionCopyPreservesFields() { + val original = RedisHsetAction( + keyGene = StringGene("key", "user:1"), + field = "age", + valueGene = StringGene("value", "30") + ) + + val copy = original.copy() as RedisHsetAction + + assertEquals(original.keyGene.value, copy.keyGene.value) + assertEquals(original.field, copy.field) + assertEquals(original.valueGene.value, copy.valueGene.value) + } + + @Test + fun testHsetActionCopyIsIndependent() { + val original = RedisHsetAction( + keyGene = StringGene("key", "user:1"), + field = "name", + valueGene = StringGene("value", "original") + ) + + val copy = original.copy() as RedisHsetAction + copy.valueGene.value = "modified" + + assertEquals("original", original.valueGene.value) + assertEquals("modified", copy.valueGene.value) + } + + @Test + fun testHsetActionFieldIsImmutable() { + val action = RedisHsetAction( + keyGene = StringGene("key", "user:1"), + field = "email", + valueGene = StringGene("value") + ) + + assertEquals("email", action.field) + } + + @Test + fun testHsetActionGetTargetKey() { + val action = RedisHsetAction( + keyGene = StringGene("key", "user:1"), + field = "name", + valueGene = StringGene("value") + ) + + assertEquals("user:1", action.getTargetKey()) + } } \ No newline at end of file diff --git a/core/src/test/kotlin/org/evomaster/core/redis/RedisDbActionTransformerTest.kt b/core/src/test/kotlin/org/evomaster/core/redis/RedisDbActionTransformerTest.kt index 7af494eb01..50f55fc894 100644 --- a/core/src/test/kotlin/org/evomaster/core/redis/RedisDbActionTransformerTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/redis/RedisDbActionTransformerTest.kt @@ -7,54 +7,26 @@ import org.junit.jupiter.api.Test class RedisDbActionTransformerTest { @Test - fun testTransformSingleAction() { - val action = RedisDbAction( - key = "user:1", - valueGene = StringGene("value", "Alice"), - dataType = RedisDbAction.RedisDataType.STRING + fun testTransformSingleHsetAction() { + val action = RedisHsetAction( + keyGene = StringGene("key", "user:1"), + field = "name", + valueGene = StringGene("value", "Alice") ) val dto = RedisDbActionTransformer.transform(listOf(action)) assertEquals(1, dto.insertions.size) + assertEquals("HSET", dto.insertions[0].command) assertEquals("user:1", dto.insertions[0].key) + assertEquals("name", dto.insertions[0].field) assertEquals("Alice", dto.insertions[0].value) } - @Test - fun testTransformMultipleActions() { - val actions = listOf( - RedisDbAction("product:1", StringGene("value", "chair"), RedisDbAction.RedisDataType.STRING), - RedisDbAction("product:2", StringGene("value", "table"), RedisDbAction.RedisDataType.STRING) - ) - - val dto = RedisDbActionTransformer.transform(actions) - - assertEquals(2, dto.insertions.size) - assertEquals("product:1", dto.insertions[0].key) - assertEquals("chair", dto.insertions[0].value) - assertEquals("product:2", dto.insertions[1].key) - assertEquals("table", dto.insertions[1].value) - } - @Test fun testTransformEmptyList() { val dto = RedisDbActionTransformer.transform(emptyList()) assertEquals(0, dto.insertions.size) } - - @Test - fun testTransformPreservesOrder() { - val keys = listOf("a", "b", "c", "d") - val actions = keys.map { - RedisDbAction(it, StringGene("value", "v_$it"), RedisDbAction.RedisDataType.STRING) - } - - val dto = RedisDbActionTransformer.transform(actions) - - keys.forEachIndexed { index, key -> - assertEquals(key, dto.insertions[index].key) - } - } } \ No newline at end of file diff --git a/core/src/test/kotlin/org/evomaster/core/redis/RedisInsertBuilderTest.kt b/core/src/test/kotlin/org/evomaster/core/redis/RedisInsertBuilderTest.kt index a085406d86..ce8f1960a9 100644 --- a/core/src/test/kotlin/org/evomaster/core/redis/RedisInsertBuilderTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/redis/RedisInsertBuilderTest.kt @@ -10,79 +10,155 @@ class RedisInsertBuilderTest { private val randomness = Randomness().apply { updateSeed(42) } - private fun failedCommand(key: String) = RedisFailedCommand().also { + private fun getCommand(key: String) = RedisFailedCommand().also { it.key = key it.command = "GET" } + private fun hgetCommand(key: String, field: String) = RedisFailedCommand().also { + it.key = key + it.field = field + it.command = "HGET" + } + + private fun hgetallCommand(key: String) = RedisFailedCommand().also { + it.key = key + it.command = "HGETALL" + } + + private fun keysCommand(pattern: String) = RedisFailedCommand().also { + it.pattern = pattern + it.command = "KEYS" + } + + // --- GET --- + @Test - fun testBuildsActionForEachFailedCommand() { - val commands = listOf(failedCommand("user:1"), failedCommand("user:2")) + fun testGetBuildsSetActionForEachCommand() { + val commands = listOf(getCommand("user:1"), getCommand("user:2")) val actions = RedisInsertBuilder.buildInsertActions(commands, emptySet(), randomness) assertEquals(2, actions.size) - assertEquals("user:1", actions[0].key) - assertEquals("user:2", actions[1].key) + actions.forEach { assertTrue(it is RedisSetAction) } + assertEquals("user:1", (actions[0] as RedisSetAction).keyGene.value) + assertEquals("user:2", (actions[1] as RedisSetAction).keyGene.value) } @Test - fun testSkipsExistingKeys() { - val commands = listOf(failedCommand("user:1"), failedCommand("user:2")) - val existing = setOf("user:1") + fun testGetSkipsExistingKeys() { + val commands = listOf(getCommand("user:1"), getCommand("user:2")) - val actions = RedisInsertBuilder.buildInsertActions(commands, existing, randomness) + val actions = RedisInsertBuilder.buildInsertActions(commands, setOf("user:1"), randomness) assertEquals(1, actions.size) - assertEquals("user:2", actions[0].key) + assertEquals("user:2", (actions[0] as RedisSetAction).keyGene.value) } @Test - fun testSkipsAllIfAllExist() { - val commands = listOf(failedCommand("user:1"), failedCommand("user:2")) - val existing = setOf("user:1", "user:2") + fun testGetKeyGeneInitializedWithObservedKey() { + val actions = RedisInsertBuilder.buildInsertActions( + listOf(getCommand("known:key")), emptySet(), randomness + ) - val actions = RedisInsertBuilder.buildInsertActions(commands, existing, randomness) + val action = actions[0] as RedisSetAction + assertEquals("known:key", action.keyGene.value) + } - assertTrue(actions.isEmpty()) + // --- HGET --- + + @Test + fun testHgetBuildsHsetAction() { + val actions = RedisInsertBuilder.buildInsertActions( + listOf(hgetCommand("user:1", "name")), emptySet(), randomness + ) + + assertEquals(1, actions.size) + val action = actions[0] as RedisHsetAction + assertEquals("user:1", action.keyGene.value) + assertEquals("name", action.field) } + // --- HGETALL --- + @Test - fun testEmptyCommandsReturnsEmptyList() { - val actions = RedisInsertBuilder.buildInsertActions(emptyList(), emptySet(), randomness) + fun testHgetallBuildsHsetActionWithPlaceholderField() { + val actions = RedisInsertBuilder.buildInsertActions( + listOf(hgetallCommand("user:1")), emptySet(), randomness + ) - assertTrue(actions.isEmpty()) + assertEquals(1, actions.size) + val action = actions[0] as RedisHsetAction + assertEquals("user:1", action.keyGene.value) + assertEquals("field", action.field) } + // --- KEYS --- + + @Test + fun testKeysBuildsSetActionWithRegexSpecialization() { + val actions = RedisInsertBuilder.buildInsertActions( + listOf(keysCommand("^user:.*$")), emptySet(), randomness + ) + + assertEquals(1, actions.size) + val action = actions[0] as RedisSetAction + // key gene should have a specialization from the pattern + assertFalse(action.keyGene.specializationGenes.isEmpty()) + } + + // --- general --- + @Test - fun testDataTypeIsString() { + fun testSkipsAllIfAllExist() { + val commands = listOf(getCommand("user:1"), getCommand("user:2")) + val actions = RedisInsertBuilder.buildInsertActions( - listOf(failedCommand("key:1")), emptySet(), randomness + commands, setOf("user:1", "user:2"), randomness ) - assertEquals(RedisDbAction.RedisDataType.STRING, actions[0].dataType) + assertTrue(actions.isEmpty()) + } + + @Test + fun testEmptyCommandsReturnsEmptyList() { + val actions = RedisInsertBuilder.buildInsertActions(emptyList(), emptySet(), randomness) + + assertTrue(actions.isEmpty()) } @Test fun testValueGeneIsInitialized() { val actions = RedisInsertBuilder.buildInsertActions( - listOf(failedCommand("key:1")), emptySet(), randomness + listOf(getCommand("key:1")), emptySet(), randomness ) - val gene = actions[0].valueGene + val gene = (actions[0] as RedisSetAction).valueGene assertInstanceOf(StringGene::class.java, gene) - // after randomize(), the value should not be the default empty string assertNotNull(gene.value) } @Test - fun testEachActionHasIndependentGene() { - val commands = listOf(failedCommand("key:1"), failedCommand("key:2")) + fun testEachActionHasIndependentValueGene() { + val commands = listOf(getCommand("key:1"), getCommand("key:2")) val actions = RedisInsertBuilder.buildInsertActions(commands, emptySet(), randomness) - // mutating one gene must not affect the other - actions[0].valueGene.value = "mutated" - assertNotEquals("mutated", actions[1].valueGene.value) + (actions[0] as RedisSetAction).valueGene.value = "mutated" + assertNotEquals("mutated", (actions[1] as RedisSetAction).valueGene.value) + } + + @Test + fun testUnsupportedCommandSkippedSilently() { + val unsupported = RedisFailedCommand().also { + it.key = "key:1" + it.command = "UNKNOWN_CMD" + } + + val actions = RedisInsertBuilder.buildInsertActions( + listOf(unsupported), emptySet(), randomness + ) + + assertTrue(actions.isEmpty()) } } \ No newline at end of file From cfef81fd9746b221500333857e663ae0a7cf8366 Mon Sep 17 00:00:00 2001 From: Alexander Szyrej Date: Tue, 19 May 2026 16:00:15 -0300 Subject: [PATCH 2/6] Fix tests --- .../redis/ReflectionBasedRedisClient.java | 6 ++ .../redis/RedisCommandExecutorTest.java | 62 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/ReflectionBasedRedisClient.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/ReflectionBasedRedisClient.java index 92df005047..e9c71e61bd 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/ReflectionBasedRedisClient.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/ReflectionBasedRedisClient.java @@ -19,6 +19,7 @@ public class ReflectionBasedRedisClient { 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 HGET_METHOD = "hget"; private static final String HGETALL_METHOD = "hgetall"; private static final String HSET_METHOD = "hset"; private static final String KEYS_METHOD = "keys"; @@ -92,6 +93,11 @@ public String getValue(String key) { return (String) invoke(GET_METHOD, key); } + /** Equivalent to HGET key field */ + public String getHashValue(String key, String field) { + return (String) invoke(HGET_METHOD, key, field); + } + /** Equivalent to KEYS * */ public Set getAllKeys() { Object result = invoke(KEYS_METHOD, "*"); diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/redis/RedisCommandExecutorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/redis/RedisCommandExecutorTest.java index 8a6d012d9f..6cc4d0e550 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/redis/RedisCommandExecutorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/redis/RedisCommandExecutorTest.java @@ -35,6 +35,7 @@ public void cleanUp() { @Test public void testInsertSingleKey() { RedisInsertionDto dto = new RedisInsertionDto(); + dto.command = "SET"; dto.key = "user:1"; dto.value = "Alice"; @@ -48,10 +49,12 @@ public void testInsertSingleKey() { @Test public void testInsertMultipleKeys() { RedisInsertionDto dto1 = new RedisInsertionDto(); + dto1.command = "SET"; dto1.key = "product:1"; dto1.value = "chair"; RedisInsertionDto dto2 = new RedisInsertionDto(); + dto2.command = "SET"; dto2.key = "product:2"; dto2.value = "table"; @@ -79,15 +82,74 @@ public void testInsertEmptyListThrows() { @Test public void testOverwritesExistingKey() { RedisInsertionDto first = new RedisInsertionDto(); + first.command = "SET"; first.key = "overwrite:key"; first.value = "old"; RedisCommandExecutor.executeInsert(client, Collections.singletonList(first)); RedisInsertionDto second = new RedisInsertionDto(); + second.command = "SET"; second.key = "overwrite:key"; second.value = "new"; RedisCommandExecutor.executeInsert(client, Collections.singletonList(second)); assertEquals("new", client.getValue("overwrite:key")); } + + @Test + public void testInsertHset() { + RedisInsertionDto dto = new RedisInsertionDto(); + dto.command = "HSET"; + dto.key = "user:1"; + dto.field = "name"; + dto.value = "Alice"; + + RedisInsertionResultsDto results = + RedisCommandExecutor.executeInsert(client, Collections.singletonList(dto)); + + assertTrue(results.executionResults.get(0)); + assertEquals("Alice", client.getHashValue("user:1", "name")); + } + + @Test + public void testInsertMultipleHset() { + RedisInsertionDto dto1 = new RedisInsertionDto(); + dto1.command = "HSET"; + dto1.key = "product:1"; + dto1.field = "name"; + dto1.value = "chair"; + + RedisInsertionDto dto2 = new RedisInsertionDto(); + dto2.command = "HSET"; + dto2.key = "product:1"; + dto2.field = "color"; + dto2.value = "red"; + + RedisInsertionResultsDto results = + RedisCommandExecutor.executeInsert(client, Arrays.asList(dto1, dto2)); + + assertTrue(results.executionResults.get(0)); + assertTrue(results.executionResults.get(1)); + assertEquals("chair", client.getHashValue("product:1", "name")); + assertEquals("red", client.getHashValue("product:1", "color")); + } + + @Test + public void testOverwritesExistingHsetField() { + RedisInsertionDto first = new RedisInsertionDto(); + first.command = "HSET"; + first.key = "overwrite:key"; + first.field = "field"; + first.value = "old"; + RedisCommandExecutor.executeInsert(client, Collections.singletonList(first)); + + RedisInsertionDto second = new RedisInsertionDto(); + second.command = "HSET"; + second.key = "overwrite:key"; + second.field = "field"; + second.value = "new"; + RedisCommandExecutor.executeInsert(client, Collections.singletonList(second)); + + assertEquals("new", client.getHashValue("overwrite:key", "field")); + } } \ No newline at end of file From 830fdfbb0d8618ef1d470aaed42b0f667d31a177 Mon Sep 17 00:00:00 2001 From: Alexander Szyrej Date: Tue, 19 May 2026 19:01:27 -0300 Subject: [PATCH 3/6] fixes associated to gene lists in actions, names and the redis writer escape library --- core/src/main/kotlin/org/evomaster/core/output/RedisWriter.kt | 2 +- .../main/kotlin/org/evomaster/core/redis/RedisHsetAction.kt | 4 ++-- .../main/kotlin/org/evomaster/core/redis/RedisSetAction.kt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/output/RedisWriter.kt b/core/src/main/kotlin/org/evomaster/core/output/RedisWriter.kt index a6ff7f5bd0..71d9c17bb8 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/RedisWriter.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/RedisWriter.kt @@ -1,6 +1,6 @@ package org.evomaster.core.output -import org.apache.commons.lang3.StringEscapeUtils +import org.apache.commons.text.StringEscapeUtils import org.evomaster.core.redis.* import org.evomaster.core.search.action.EvaluatedRedisDbAction diff --git a/core/src/main/kotlin/org/evomaster/core/redis/RedisHsetAction.kt b/core/src/main/kotlin/org/evomaster/core/redis/RedisHsetAction.kt index 1ce231ae01..8904486085 100644 --- a/core/src/main/kotlin/org/evomaster/core/redis/RedisHsetAction.kt +++ b/core/src/main/kotlin/org/evomaster/core/redis/RedisHsetAction.kt @@ -22,10 +22,10 @@ class RedisHsetAction( override fun getTargetKey() = keyGene.value - override fun seeTopGenes(): List = listOf(valueGene) + override fun seeTopGenes(): List = listOf(keyGene, valueGene) override fun copyContent(): Action = RedisHsetAction(keyGene, field, valueGene.copy() as StringGene) - override fun getName() = "Redis_HSET_${keyGene}_$field" + override fun getName() = "Redis_HSET_${keyGene.value}_$field" } \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/redis/RedisSetAction.kt b/core/src/main/kotlin/org/evomaster/core/redis/RedisSetAction.kt index fe76efacdd..6035b857e4 100644 --- a/core/src/main/kotlin/org/evomaster/core/redis/RedisSetAction.kt +++ b/core/src/main/kotlin/org/evomaster/core/redis/RedisSetAction.kt @@ -17,10 +17,10 @@ class RedisSetAction( override fun getTargetKey() = keyGene.value - override fun seeTopGenes(): List = listOf(valueGene) + override fun seeTopGenes(): List = listOf(keyGene, valueGene) override fun copyContent(): Action = RedisSetAction(keyGene, valueGene.copy() as StringGene) - override fun getName() = "Redis_SET_$keyGene" + override fun getName() = "Redis_SET_${keyGene.value}" } \ No newline at end of file From f681de54a165201c6011389791f5bd04e13da59d Mon Sep 17 00:00:00 2001 From: Alexander Szyrej Date: Wed, 20 May 2026 13:23:08 -0300 Subject: [PATCH 4/6] increased timeout for HGET and HGETALL tests, since it includes both methods --- .../com/foo/spring/rest/redis/RedisController.java | 9 +++++---- .../RedisLettuceFindHashNoSaveEMTest.java | 2 +- .../problem/enterprise/service/EnterpriseFitness.kt | 1 + .../org/evomaster/core/redis/RedisHsetAction.kt | 11 ++++++++--- .../kotlin/org/evomaster/core/redis/RedisSetAction.kt | 11 ++++++++--- 5 files changed, 23 insertions(+), 11 deletions(-) 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 index 63a279cb7c..44d80e8a2b 100644 --- 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 @@ -31,6 +31,8 @@ public abstract class RedisController extends EmbeddedSutController { private String host; private int port; + private ReflectionBasedRedisClient reflectionRedisClient; + protected RedisController(String databaseName, Class redisAppClass) { this.databaseName = databaseName; this.redisAppClass = redisAppClass; @@ -50,15 +52,14 @@ public String startSut() { this.port = port; redisClient = new Jedis(host, port); + reflectionRedisClient = new ReflectionBasedRedisClient(host, port, 0); SpringApplicationBuilder app = new SpringApplicationBuilder(redisAppClass); - app.properties( "--server.port=0", "spring.data.redis.host=" + host, "spring.data.redis.port=" + port ); - ctx = app.run(); resetStateOfSUT(); @@ -67,9 +68,9 @@ public String startSut() { @Override public void stopSut() { - redisContainer.stop(); ctx.stop(); ctx.close(); + redisContainer.stop(); } @Override @@ -113,6 +114,6 @@ protected int getSutPort() { @Override public ReflectionBasedRedisClient getRedisConnection() { - return new ReflectionBasedRedisClient(this.host, this.port, 0); + return reflectionRedisClient; // ← instancia cacheada, no new } } diff --git a/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/org/evomaster/e2etests/spring/rest/redis/lettuce/findhashnosave/RedisLettuceFindHashNoSaveEMTest.java b/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/org/evomaster/e2etests/spring/rest/redis/lettuce/findhashnosave/RedisLettuceFindHashNoSaveEMTest.java index fdd4cedad0..a49fa9ce51 100644 --- a/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/org/evomaster/e2etests/spring/rest/redis/lettuce/findhashnosave/RedisLettuceFindHashNoSaveEMTest.java +++ b/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/org/evomaster/e2etests/spring/rest/redis/lettuce/findhashnosave/RedisLettuceFindHashNoSaveEMTest.java @@ -43,7 +43,7 @@ public void testFindHashNoSaveEM() throws Throwable { assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/redislettucefindhashnosave/findHashAllFields", null); assertHasAtLeastOne(solution, HttpVerb.GET, 404, "/redislettucefindhashnosave/findHashAllFields", null); }, - 3); + 6); } } 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 b9713424e9..86880f898e 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 @@ -229,6 +229,7 @@ abstract class EnterpriseFitness : FitnessFunction() where T : Individual val results = rc.executeRedisDatabaseInsertions(dto) results?.executionResults?.forEachIndexed { index, b -> + if (!b) println("FAILED insertion $index: ${allRedisActions[index].getName()}") redisResults[index].setInsertExecutionResult(b) } diff --git a/core/src/main/kotlin/org/evomaster/core/redis/RedisHsetAction.kt b/core/src/main/kotlin/org/evomaster/core/redis/RedisHsetAction.kt index 8904486085..b2ee170042 100644 --- a/core/src/main/kotlin/org/evomaster/core/redis/RedisHsetAction.kt +++ b/core/src/main/kotlin/org/evomaster/core/redis/RedisHsetAction.kt @@ -17,15 +17,20 @@ import org.evomaster.core.search.gene.string.StringGene class RedisHsetAction( val keyGene: StringGene, val field: String, - val valueGene: StringGene + val valueGene: StringGene, + private val nameKey: String = keyGene.value ) : RedisDbAction() { + init { + addChildren(listOf(keyGene, valueGene)) + } + override fun getTargetKey() = keyGene.value override fun seeTopGenes(): List = listOf(keyGene, valueGene) override fun copyContent(): Action = - RedisHsetAction(keyGene, field, valueGene.copy() as StringGene) + RedisHsetAction(keyGene.copy() as StringGene, field, valueGene.copy() as StringGene, nameKey) - override fun getName() = "Redis_HSET_${keyGene.value}_$field" + override fun getName() = "Redis_HSET_${nameKey}_$field" } \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/redis/RedisSetAction.kt b/core/src/main/kotlin/org/evomaster/core/redis/RedisSetAction.kt index 6035b857e4..6806017401 100644 --- a/core/src/main/kotlin/org/evomaster/core/redis/RedisSetAction.kt +++ b/core/src/main/kotlin/org/evomaster/core/redis/RedisSetAction.kt @@ -12,15 +12,20 @@ import org.evomaster.core.search.gene.string.StringGene */ class RedisSetAction( val keyGene: StringGene, - val valueGene: StringGene + val valueGene: StringGene, + private val nameKey: String = keyGene.value ) : RedisDbAction() { + init { + addChildren(listOf(keyGene, valueGene)) + } + override fun getTargetKey() = keyGene.value override fun seeTopGenes(): List = listOf(keyGene, valueGene) override fun copyContent(): Action = - RedisSetAction(keyGene, valueGene.copy() as StringGene) + RedisSetAction(keyGene.copy() as StringGene, valueGene.copy() as StringGene, nameKey) - override fun getName() = "Redis_SET_${keyGene.value}" + override fun getName() = "Redis_SET_${nameKey}" } \ No newline at end of file From 9a92a5a4a201454eab888566ee35ede2b9afd35f Mon Sep 17 00:00:00 2001 From: Alexander Szyrej Date: Fri, 22 May 2026 11:46:43 -0300 Subject: [PATCH 5/6] Explained failed commands as in those which return no data --- .../client/java/controller/internal/db/redis/RedisHandler.java | 3 ++- .../main/kotlin/org/evomaster/core/redis/RedisHsetAction.kt | 3 ++- .../src/main/kotlin/org/evomaster/core/redis/RedisSetAction.kt | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) 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 38e626545a..f511c0fa7f 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 @@ -150,7 +150,8 @@ private RedisFailedCommand createFailedCommand(RedisCommand redisCommand) { args.get(1)); } default: - throw new RuntimeException("Invalid command registering failed redis commands."); + throw new RuntimeException( + "Invalid command registering failed redis commands. Type encountered: " + type); } } diff --git a/core/src/main/kotlin/org/evomaster/core/redis/RedisHsetAction.kt b/core/src/main/kotlin/org/evomaster/core/redis/RedisHsetAction.kt index b2ee170042..beb71f6e3a 100644 --- a/core/src/main/kotlin/org/evomaster/core/redis/RedisHsetAction.kt +++ b/core/src/main/kotlin/org/evomaster/core/redis/RedisHsetAction.kt @@ -5,7 +5,8 @@ import org.evomaster.core.search.gene.Gene import org.evomaster.core.search.gene.string.StringGene /** - * Represents an HSET action, generated from a failed HGET or HGETALL command. + * Represents an HSET action, generated from a failed HGET or HGETALL command + * (failed meaning HGET or HGETALL commands that return no data when executed). * *

For HGET, [field] is the exact field observed in the failed command. * For HGETALL, [field] is a placeholder since the expected fields are unknown. diff --git a/core/src/main/kotlin/org/evomaster/core/redis/RedisSetAction.kt b/core/src/main/kotlin/org/evomaster/core/redis/RedisSetAction.kt index 6806017401..82777ba280 100644 --- a/core/src/main/kotlin/org/evomaster/core/redis/RedisSetAction.kt +++ b/core/src/main/kotlin/org/evomaster/core/redis/RedisSetAction.kt @@ -6,6 +6,7 @@ import org.evomaster.core.search.gene.string.StringGene /** * Represents a SET action, generated from a failed GET command. + * (failed meaning GET commands that return no data when executed). * * @param keyGene the new key to be inserted * @param valueGene gene representing the string value to insert From f516d28dca65eb9eae969dc9c607c5fc2a09f8c5 Mon Sep 17 00:00:00 2001 From: Alexander Szyrej Date: Thu, 28 May 2026 13:20:14 -0300 Subject: [PATCH 6/6] using printable instead of raw for genes in redisWriter --- .../kotlin/org/evomaster/core/output/RedisWriter.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/output/RedisWriter.kt b/core/src/main/kotlin/org/evomaster/core/output/RedisWriter.kt index 71d9c17bb8..7ea7f82f62 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/RedisWriter.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/RedisWriter.kt @@ -47,15 +47,15 @@ object RedisWriter { val dslCall = when (action) { is RedisSetAction -> { - val key = escape(action.keyGene.getValueAsRawString(), format) - val value = escape(action.valueGene.getValueAsRawString(), format) - ".set(\"$key\", \"$value\")" + val key = action.keyGene.getValueAsPrintableString(targetFormat = format) + val value = action.valueGene.getValueAsPrintableString(targetFormat = format) + ".set($key, $value)" } is RedisHsetAction -> { - val key = escape(action.keyGene.getValueAsRawString(), format) + val key = action.keyGene.getValueAsPrintableString(targetFormat = format) val field = escape(action.field, format) - val value = escape(action.valueGene.getValueAsRawString(), format) - ".hset(\"$key\", \"$field\", \"$value\")" + val value = action.valueGene.getValueAsPrintableString(targetFormat = format) + ".hset($key, \"$field\", $value)" } }