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 dfcdc180c6..601cca9178 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 @@ -1,5 +1,8 @@ package org.evomaster.client.java.controller.api.dto.database.execution; +import java.util.ArrayList; +import java.util.List; + /** * Each time a Redis command is executed and returns no data, we keep track of which keys were involved, * as well as relevant information such as the command type. @@ -12,9 +15,9 @@ public class RedisFailedCommand { public String command; /** - * Key involved. Could be null if the command does not have a key in the arguments. For example: KEYS (pattern). + * Keys involved. Could be null if the command does not have any key in the arguments. For example: KEYS (pattern). */ - public String key; + public List keys; /** * Pattern involved. It'd only apply to commands with pattern like KEYS. @@ -28,9 +31,9 @@ public class RedisFailedCommand { public RedisFailedCommand() {} - public RedisFailedCommand(String command, String key, String pattern, String field) { + public RedisFailedCommand(String command, List keys, String pattern, String field) { this.command = command; - this.key = key; + this.keys = new ArrayList<>(keys); this.pattern = pattern; this.field = field; } 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 f511c0fa7f..580d09bc23 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 @@ -105,9 +105,15 @@ public List getEvaluatedRedisCommands() { private void registerFailedCommand(RedisCommand redisCommand, double distance) { RedisCommand.RedisCommandType type = redisCommand.getType(); - if (distance > 0 && - type.equals(GET) || type.equals(KEYS) || type.equals(HGET) || type.equals(HGETALL)) { - //For this first iteration we'll only work on GET commands. + if (distance > 0 && ( + type.equals(GET) || + type.equals(HGET) || + type.equals(HGETALL) || + type.equals(KEYS) || + type.equals(SINTER) || + type.equals(SMEMBERS)) + ) { + // Further commands will be registered in future iterations. failedCommands.add(createFailedCommand(redisCommand)); } } @@ -117,13 +123,15 @@ private RedisFailedCommand createFailedCommand(RedisCommand redisCommand) { List args = redisCommand.extractArgs(); switch (type) { case GET: - case HGETALL: { + case HGETALL: + case SINTER: + case SMEMBERS: { if (args.isEmpty()) { throw new IllegalArgumentException("Command " + type.getLabel() + " has invalid arguments."); } return new RedisFailedCommand( type.getLabel().toUpperCase(), - args.get(0), + args, null, null); } @@ -134,7 +142,7 @@ private RedisFailedCommand createFailedCommand(RedisCommand redisCommand) { } return new RedisFailedCommand( type.getLabel().toUpperCase(), - null, + Collections.emptyList(), RedisUtils.redisPatternToRegex(args.get(0)), null); } @@ -145,7 +153,7 @@ private RedisFailedCommand createFailedCommand(RedisCommand redisCommand) { } return new RedisFailedCommand( type.getLabel().toUpperCase(), - args.get(0), + Collections.singletonList((args.get(0))), null, args.get(1)); } @@ -207,8 +215,7 @@ private RedisKeyValueStore createRedisInfoForIntersection(List commandKe //The value for each one, since each key represents a SET, correspond to the members of that given set. Map redisData = new HashMap<>(); keySet.forEach( - key -> redisData.put(key, new RedisValueData(redisClient.getSetMembers(key)) - )); + key -> redisData.put(key, new RedisValueData(redisClient.getSetMembers(key)))); return new RedisKeyValueStore(redisData); } @@ -219,8 +226,7 @@ private RedisKeyValueStore createRedisInfoForAllKeys(ReflectionBasedRedisClient //No value is needed in this case. Map redisData = new HashMap<>(); keys.forEach( - key -> redisData.put(key, null) - ); + key -> redisData.put(key, null)); return new RedisKeyValueStore(redisData); } @@ -230,7 +236,8 @@ private RedisKeyValueStore createRedisInfoForKeysByType(String type, ReflectionB //A Map structure is introduced here using the same keys that are stored in REDIS. //No value is needed in this case. Map redisData = new HashMap<>(); - keys.forEach(key -> redisData.put(key, null)); + keys.forEach( + key -> redisData.put(key, null)); return new RedisKeyValueStore(redisData); } @@ -240,7 +247,8 @@ private RedisKeyValueStore createRedisInfoForKeysByField(ReflectionBasedRedisCli //A Map structure is introduced here using the same keys that are stored in REDIS. //The value for each one, since each key is of type HASH, correspond to the fields stored for that given key. Map redisData = new HashMap<>(); - keys.forEach(key -> redisData.put(key, new RedisValueData(redisClient.getHashFields(key)))); + keys.forEach( + key -> redisData.put(key, new RedisValueData(redisClient.getHashFields(key)))); return new RedisKeyValueStore(redisData); } 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 d8d9c39411..3afa827406 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 @@ -37,6 +37,9 @@ public static RedisInsertionResultsDto executeInsert( case "HSET": client.hashSet(dto.key, dto.field, dto.value); break; + case "SADD": + client.addMember(dto.key, dto.value); + break; default: throw new IllegalArgumentException( "Unsupported Redis command: " + dto.command); 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 e9c71e61bd..df72a2e4da 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 @@ -23,6 +23,7 @@ public class ReflectionBasedRedisClient { 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 SADD_METHOD = "sadd"; private static final String SELECT_METHOD = "select"; private static final String SET_METHOD = "set"; private static final String SHUTDOWN_METHOD = "shutdown"; @@ -117,6 +118,11 @@ public void hashSet(String key, String field, String value) { invoke(HSET_METHOD, key, field, value); } + /** SADD key */ + public void addMember(String key, String member) { + invoke(SADD_METHOD, key, new String[]{member}); + } + /** SMEMBERS key */ public Set getSetMembers(String key) { Object result = invoke(SMEMBERS_METHOD, key); 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 040f17f075..a01f3c012d 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 @@ -71,6 +71,23 @@ public RedisStatementDsl hset(String key, String field, String value) { return this; } + @Override + public RedisStatementDsl sadd(String key, String member) { + checkDsl(); + if (key == null || key.isEmpty()) { + throw new IllegalArgumentException("Unspecified key"); + } + if (member == null || member.isEmpty()) { + throw new IllegalArgumentException("Unspecified member"); + } + RedisInsertionDto dto = new RedisInsertionDto(); + dto.command = "SADD"; + dto.key = key; + dto.value = member; + 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 5908fd6c63..e4526cf237 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 @@ -20,4 +20,13 @@ public interface RedisSequenceDsl { * @return a statement object on which the sequence can be continued or closed */ RedisStatementDsl hset(String key, String field, String value); + + /** + * A SADD operation on the Redis database. + * + * @param key the set key. + * @param member the new member in that set. + * @return a statement object on which the sequence can be continued or closed + */ + RedisStatementDsl sadd(String key, String member); } diff --git a/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/setintersectionnosave/RedisLettuceSetIntersectionNoSaveApp.java b/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/setintersectionnosave/RedisLettuceSetIntersectionNoSaveApp.java new file mode 100644 index 0000000000..d89da6c3fc --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/setintersectionnosave/RedisLettuceSetIntersectionNoSaveApp.java @@ -0,0 +1,20 @@ +package com.redis.lettuce.setintersectionnosave; + +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 RedisLettuceSetIntersectionNoSaveApp extends SwaggerConfiguration { + public RedisLettuceSetIntersectionNoSaveApp() { + super("redislettucesetintersectionnosave"); + } + + public static void main(String[] args) { + SpringApplication.run(RedisLettuceSetIntersectionNoSaveApp.class, args); + } + +} diff --git a/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/setintersectionnosave/RedisLettuceSetIntersectionNoSaveRest.java b/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/setintersectionnosave/RedisLettuceSetIntersectionNoSaveRest.java new file mode 100644 index 0000000000..639691f9a4 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/setintersectionnosave/RedisLettuceSetIntersectionNoSaveRest.java @@ -0,0 +1,25 @@ +package com.redis.lettuce.setintersectionnosave; + +import com.redis.lettuce.AbstractRedisLettuceRest; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Set; + +@RestController +@RequestMapping(path = "/redislettucesetintersectionnosave") +public class RedisLettuceSetIntersectionNoSaveRest extends AbstractRedisLettuceRest { + + @GetMapping("/set/variable-intersection/{set1}/{set2}") + public ResponseEntity getIntersection(@PathVariable String set1, @PathVariable String set2) { + Set result = sync.sinter(set1, set2); + 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/setmembersnosave/RedisLettuceSetMembersNoSaveApp.java b/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/setmembersnosave/RedisLettuceSetMembersNoSaveApp.java new file mode 100644 index 0000000000..1987f79570 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/setmembersnosave/RedisLettuceSetMembersNoSaveApp.java @@ -0,0 +1,20 @@ +package com.redis.lettuce.setmembersnosave; + +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 RedisLettuceSetMembersNoSaveApp extends SwaggerConfiguration { + public RedisLettuceSetMembersNoSaveApp() { + super("redislettucesetmembersnosave"); + } + + public static void main(String[] args) { + SpringApplication.run(RedisLettuceSetMembersNoSaveApp.class, args); + } + +} diff --git a/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/setmembersnosave/RedisLettuceSetMembersNoSaveRest.java b/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/setmembersnosave/RedisLettuceSetMembersNoSaveRest.java new file mode 100644 index 0000000000..368b680689 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-redis/src/main/java/com/redis/lettuce/setmembersnosave/RedisLettuceSetMembersNoSaveRest.java @@ -0,0 +1,25 @@ +package com.redis.lettuce.setmembersnosave; + +import com.redis.lettuce.AbstractRedisLettuceRest; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Set; + +@RestController +@RequestMapping(path = "/redislettucesetmembersnosave") +public class RedisLettuceSetMembersNoSaveRest extends AbstractRedisLettuceRest { + + @GetMapping("/set/members/{key}") + public ResponseEntity getMembers(@PathVariable String key) { + Set result = sync.smembers(key); + 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/test/java/com/foo/spring/rest/redis/lettuce/setintersectionnosave/RedisLettuceSetIntersectionNoSaveController.java b/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/com/foo/spring/rest/redis/lettuce/setintersectionnosave/RedisLettuceSetIntersectionNoSaveController.java new file mode 100644 index 0000000000..4216a884ed --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/com/foo/spring/rest/redis/lettuce/setintersectionnosave/RedisLettuceSetIntersectionNoSaveController.java @@ -0,0 +1,15 @@ +package com.foo.spring.rest.redis.lettuce.setintersectionnosave; + +import com.foo.spring.rest.redis.RedisController; +import com.redis.lettuce.setintersectionnosave.RedisLettuceSetIntersectionNoSaveApp; + +public class RedisLettuceSetIntersectionNoSaveController extends RedisController { + public RedisLettuceSetIntersectionNoSaveController() { + super("lettuce", RedisLettuceSetIntersectionNoSaveApp.class); + } + + @Override + public String getPackagePrefixesToCover() { + return "com.redis.lettuce.setintersectionnosave"; + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/com/foo/spring/rest/redis/lettuce/setmembersnosave/RedisLettuceSetMembersNoSaveController.java b/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/com/foo/spring/rest/redis/lettuce/setmembersnosave/RedisLettuceSetMembersNoSaveController.java new file mode 100644 index 0000000000..c0df17e7bd --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/com/foo/spring/rest/redis/lettuce/setmembersnosave/RedisLettuceSetMembersNoSaveController.java @@ -0,0 +1,15 @@ +package com.foo.spring.rest.redis.lettuce.setmembersnosave; + +import com.foo.spring.rest.redis.RedisController; +import com.redis.lettuce.setmembersnosave.RedisLettuceSetMembersNoSaveApp; + +public class RedisLettuceSetMembersNoSaveController extends RedisController { + public RedisLettuceSetMembersNoSaveController() { + super("lettuce", RedisLettuceSetMembersNoSaveApp.class); + } + + @Override + public String getPackagePrefixesToCover() { + return "com.redis.lettuce.setmembersnosave"; + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/org/evomaster/e2etests/spring/rest/redis/lettuce/setintersectionnosave/RedisLettuceSetIntersectionNoSaveEMTest.java b/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/org/evomaster/e2etests/spring/rest/redis/lettuce/setintersectionnosave/RedisLettuceSetIntersectionNoSaveEMTest.java new file mode 100644 index 0000000000..e680c02bd5 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/org/evomaster/e2etests/spring/rest/redis/lettuce/setintersectionnosave/RedisLettuceSetIntersectionNoSaveEMTest.java @@ -0,0 +1,49 @@ +package org.evomaster.e2etests.spring.rest.redis.lettuce.setintersectionnosave; + +import com.foo.spring.rest.redis.lettuce.setintersectionnosave.RedisLettuceSetIntersectionNoSaveController; +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 RedisLettuceSetIntersectionNoSaveEMTest extends RestTestBase { + + @BeforeAll + public static void initClass() throws Exception { + + EMConfig config = new EMConfig(); + config.setInstrumentMR_REDIS(true); + RestTestBase.initClass(new RedisLettuceSetIntersectionNoSaveController(), config); + } + + @Test + public void testSetIntersectionNoSaveEM() throws Throwable { + + runTestHandlingFlakyAndCompilation( + "RedisLettuceSetIntersectionNoSaveEM", + "org.foo.spring.rest.redis.RedisLettuceSetIntersectionNoSaveEM", + 2000, + true, + (args) -> { + setOption(args, "maxEvaluations", "10"); + 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, "/redislettucesetintersectionnosave/set/variable-intersection/{set1}/{set2}", null); + assertHasAtLeastOne(solution, HttpVerb.GET, 404, "/redislettucesetintersectionnosave/set/variable-intersection/{set1}/{set2}", null); + + }, + 6); + + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/org/evomaster/e2etests/spring/rest/redis/lettuce/setmembersnosave/RedisLettuceSetMembersNoSaveEMTest.java b/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/org/evomaster/e2etests/spring/rest/redis/lettuce/setmembersnosave/RedisLettuceSetMembersNoSaveEMTest.java new file mode 100644 index 0000000000..3f36657782 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-redis/src/test/java/org/evomaster/e2etests/spring/rest/redis/lettuce/setmembersnosave/RedisLettuceSetMembersNoSaveEMTest.java @@ -0,0 +1,49 @@ +package org.evomaster.e2etests.spring.rest.redis.lettuce.setmembersnosave; + +import com.foo.spring.rest.redis.lettuce.setmembersnosave.RedisLettuceSetMembersNoSaveController; +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 RedisLettuceSetMembersNoSaveEMTest extends RestTestBase { + + @BeforeAll + public static void initClass() throws Exception { + + EMConfig config = new EMConfig(); + config.setInstrumentMR_REDIS(true); + RestTestBase.initClass(new RedisLettuceSetMembersNoSaveController(), config); + } + + @Test + public void testSetMembersNoSaveEM() throws Throwable { + + runTestHandlingFlakyAndCompilation( + "RedisLettuceSetMembersNoSaveEM", + "org.foo.spring.rest.redis.RedisLettuceSetMembersNoSaveEM", + 1000, + true, + (args) -> { + setOption(args, "maxEvaluations", "10"); + 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, "/redislettucesetmembersnosave/set/members/{key}", null); + assertHasAtLeastOne(solution, HttpVerb.GET, 404, "/redislettucesetmembersnosave/set/members/{key}", null); + + }, + 3); + + } +} 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 ca2e69bc30..32810c665a 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/RedisWriter.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/RedisWriter.kt @@ -44,43 +44,22 @@ object RedisWriter { val insertionVarResult = "${insertionVar}_result" val previousVar = insertionVars.joinToString(", ") { it.first } - redisDbInitialization + val dslCalls = redisDbInitialization .filter { !skipFailure || it.redisResult.getInsertExecutionResult() } - .forEachIndexed { index, evaluated -> + .flatMap { evaluated -> toDslCalls(evaluated.redisAction, format) } - val action = evaluated.redisAction - - val dslCall: String = when (action) { - is RedisSetAction -> { - val key = "\"${escape(action.key, format)}\"" - val value = action.valueGene.getValueAsPrintableString(targetFormat = format) - ".set($key, $value)" - } - is RedisSetFromPatternAction -> { - val key = action.keyGene.getValueAsPrintableString(targetFormat = format) - val value = action.valueGene.getValueAsPrintableString(targetFormat = format) - ".set($key, $value)" - } - is RedisHsetAction -> { - val key = "\"${escape(action.key, format)}\"" - val field = escape(action.field, format) - val value = action.valueGene.getValueAsPrintableString(targetFormat = format) - ".hset($key, \"$field\", $value)" - } - } - - lines.add( - when { - index == 0 && format.isJava() -> - "List $insertionVar = redis($previousVar)" - index == 0 && format.isKotlin() -> - "val $insertionVar = redis($previousVar)" - else -> ".and()" - } + dslCall - ) - - if (index == 0) lines.indent() - } + dslCalls.forEachIndexed { index, dslCall -> + lines.add( + when { + index == 0 && format.isJava() -> + "List $insertionVar = redis($previousVar)" + index == 0 && format.isKotlin() -> + "val $insertionVar = redis($previousVar)" + else -> ".and()" + } + dslCall + ) + if (index == 0) lines.indent() + } lines.add(".dtos()") lines.appendSemicolon() @@ -100,9 +79,41 @@ object RedisWriter { insertionVars.add(insertionVar to insertionVarResult) } + private fun toDslCalls(action: RedisDbAction, format: OutputFormat): List { + return when (action) { + is RedisSetAction -> { + val key = "\"${escape(action.key, format)}\"" + val value = action.valueGene.getValueAsPrintableString(targetFormat = format) + listOf(".set($key, $value)") + } + is RedisSetFromPatternAction -> { + val key = action.keyGene.getValueAsPrintableString(targetFormat = format) + val value = action.valueGene.getValueAsPrintableString(targetFormat = format) + listOf(".set($key, $value)") + } + is RedisHsetAction -> { + val key = "\"${escape(action.key, format)}\"" + val field = escape(action.field, format) + val value = action.valueGene.getValueAsPrintableString(targetFormat = format) + listOf(".hset($key, \"$field\", $value)") + } + is RedisSaddAction -> { + val key = "\"${escape(action.key, format)}\"" + val member = action.memberGene.getValueAsPrintableString(targetFormat = format) + listOf(".sadd($key, $member)") + } + is RedisSaddFromSinterAction -> { + val member = action.memberGene.getValueAsPrintableString(targetFormat = format) + action.keys.map { key -> + ".sadd(\"${escape(key, format)}\", $member)" + } + } + } + } + private fun escape(value: String, format: OutputFormat): String { return StringEscapeUtils.escapeJava(value).let { if (format.isKotlin()) it.replace("$", "\\$") else it } } -} +} \ No newline at end of file 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 86880f898e..213464fa7b 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 @@ -228,9 +228,18 @@ abstract class EnterpriseFitness : FitnessFunction() where T : Individual val dto = RedisDbActionTransformer.transform(allRedisActions) val results = rc.executeRedisDatabaseInsertions(dto) - results?.executionResults?.forEachIndexed { index, b -> - if (!b) println("FAILED insertion $index: ${allRedisActions[index].getName()}") - redisResults[index].setInsertExecutionResult(b) + if (results?.executionResults != null) { + var dtoIndex = 0 + allRedisActions.forEachIndexed { actionIndex, action -> + // Now actions such as SaddFromSinter have multiple insertion dtos related. + val count = action.insertionsCount() + val success = (dtoIndex until dtoIndex + count).all { + results.executionResults[it] + } + if (!success) println("FAILED insertion $actionIndex: ${action.getName()}") + redisResults[actionIndex].setInsertExecutionResult(success) + dtoIndex += count + } } return true 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 451c5dd302..3c203a3c3e 100644 --- a/core/src/main/kotlin/org/evomaster/core/redis/RedisDbAction.kt +++ b/core/src/main/kotlin/org/evomaster/core/redis/RedisDbAction.kt @@ -15,5 +15,7 @@ sealed class RedisDbAction : EnvironmentAction(listOf()) { */ open fun getTargetKey(): String? = null + open fun insertionsCount(): Int = 1 + 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 113e7fc11d..4314b435bb 100644 --- a/core/src/main/kotlin/org/evomaster/core/redis/RedisDbActionTransformer.kt +++ b/core/src/main/kotlin/org/evomaster/core/redis/RedisDbActionTransformer.kt @@ -9,23 +9,35 @@ object RedisDbActionTransformer { fun transform(actions: List): RedisDatabaseCommandsDto { val dto = RedisDatabaseCommandsDto() - dto.insertions = actions.map { action -> + dto.insertions = actions.flatMap { action -> when (action) { - is RedisHsetAction -> RedisInsertionDto().also { - it.command = "HSET" - it.key = action.key - it.field = action.field - it.value = action.valueGene.value - } - is RedisSetAction -> RedisInsertionDto().also { + is RedisSetAction -> listOf(RedisInsertionDto().also { it.command = "SET" it.key = action.key it.value = action.valueGene.value - } - is RedisSetFromPatternAction -> RedisInsertionDto().also { + }) + is RedisSetFromPatternAction -> listOf(RedisInsertionDto().also { it.command = "SET" it.key = action.keyGene.getValueAsRawString() it.value = action.valueGene.value + }) + is RedisHsetAction -> listOf(RedisInsertionDto().also { + it.command = "HSET" + it.key = action.key + it.field = action.field + it.value = action.valueGene.value + }) + is RedisSaddAction -> listOf(RedisInsertionDto().also { + it.command = "SADD" + it.key = action.key + it.value = action.memberGene.value + }) + is RedisSaddFromSinterAction -> action.keys.map { key -> + RedisInsertionDto().also { + it.command = "SADD" + it.key = key + it.value = action.memberGene.value + } } } } 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 afa5c737bf..bab564790a 100644 --- a/core/src/main/kotlin/org/evomaster/core/redis/RedisInsertBuilder.kt +++ b/core/src/main/kotlin/org/evomaster/core/redis/RedisInsertBuilder.kt @@ -28,29 +28,50 @@ object RedisInsertBuilder { failedCommands: List, existingKeys: Set ): List { - return failedCommands - .filter { it.key == null || it.key !in existingKeys } - .flatMap { cmd -> buildActionsForCommand(cmd) } + return failedCommands.flatMap { cmd -> + buildActionsForCommand(cmd, existingKeys) + } } private fun buildActionsForCommand( - cmd: RedisFailedCommand + cmd: RedisFailedCommand, + existingKeys: Set ): List { + val keys = cmd.keys ?: emptyList() + return when (cmd.command) { - "GET" -> listOf(RedisSetAction( - key = cmd.key!!, - valueGene = StringGene("value") - )) - "HGET" -> listOf(RedisHsetAction( - key = cmd.key!!, - field = cmd.field ?: "field", - valueGene = StringGene("value") - )) - "HGETALL" -> listOf(RedisHsetAction( - key = cmd.key!!, - field = "field", - valueGene = StringGene("value") - )) + "GET" -> { + val key = keys.firstOrNull() ?: return emptyList() + if (key in existingKeys) return emptyList() + listOf( + RedisSetAction( + key = key, + valueGene = StringGene("value") + ) + ) + } + "HGET" -> { + val key = keys.firstOrNull() ?: return emptyList() + if (key in existingKeys) return emptyList() + listOf( + RedisHsetAction( + key = key, + field = cmd.field ?: "field", + valueGene = StringGene("value") + ) + ) + } + "HGETALL" -> { + val key = keys.firstOrNull() ?: return emptyList() + if (key in existingKeys) return emptyList() + listOf( + RedisHsetAction( + key = key, + field = "field", + valueGene = StringGene("value") + ) + ) + } "KEYS" -> { val pattern = cmd.pattern ?: return emptyList() val keyGene = RegexHandler.createGeneForJVM(pattern) @@ -59,6 +80,21 @@ object RedisInsertBuilder { valueGene = StringGene("value") )) } + "SMEMBERS" -> { + val key = keys.firstOrNull() ?: return emptyList() + if (key in existingKeys) return emptyList() + listOf(RedisSaddAction( + key = key, + memberGene = StringGene("member") + )) + } + "SINTER" -> { + if (keys.isEmpty()) return emptyList() + listOf(RedisSaddFromSinterAction( + keys = keys, + memberGene = StringGene("member") + )) + } else -> { LoggingUtil.uniqueWarn(log, "Unsupported Redis command for insert action: ${cmd.command}") assert(false) { "Unsupported Redis command: ${cmd.command}" } diff --git a/core/src/main/kotlin/org/evomaster/core/redis/RedisSaddAction.kt b/core/src/main/kotlin/org/evomaster/core/redis/RedisSaddAction.kt new file mode 100644 index 0000000000..880a709f96 --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/redis/RedisSaddAction.kt @@ -0,0 +1,34 @@ +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 SADD action, generated from a failed SMEMBERS command + * (failed meaning those commands return no data when executed). + * + *

For SMEMBERS, a single [RedisSaddAction] is generated for the observed key, + * inserting one element so the set is non-empty. + * + * @param key the key of the set to insert into. + * @param memberGene gene representing the element to add to the set. + */ +class RedisSaddAction( + val key: String, + val memberGene: StringGene +) : RedisDbAction() { + + init { + addChildren(listOf(memberGene)) + } + + override fun getTargetKey() = key + + override fun seeTopGenes(): List = listOf(memberGene) + + override fun copyContent(): Action = + RedisSaddAction(key, memberGene.copy() as StringGene) + + override fun getName() = "Redis_SADD_${key}" +} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/redis/RedisSaddFromSinterAction.kt b/core/src/main/kotlin/org/evomaster/core/redis/RedisSaddFromSinterAction.kt new file mode 100644 index 0000000000..f96a7b13cb --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/redis/RedisSaddFromSinterAction.kt @@ -0,0 +1,36 @@ +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 single SADD action generated from a failed SINTER command. + * Instead of one action per key, this action holds all keys involved in the + * intersection and a single [memberGene] that will be added to every set, + * guaranteeing a non-empty intersection. + * + * @param keys all keys involved in the SINTER command. + * @param memberGene gene representing the element to insert into each set. + */ +class RedisSaddFromSinterAction( + val keys: List, + val memberGene: StringGene +) : RedisDbAction() { + + init { + require(keys.isNotEmpty()) { "RedisSaddFromSinterAction requires at least one key" } + addChildren(listOf(memberGene)) + } + + override fun getTargetKey() = keys.joinToString(",") + + override fun insertionsCount(): Int = keys.size + + override fun seeTopGenes(): List = listOf(memberGene) + + override fun copyContent(): Action = + RedisSaddFromSinterAction(keys, memberGene.copy() as StringGene) + + override fun getName() = "Redis_SADD_SINTER_${keys.joinToString("_")}" +} \ No newline at end of file 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 67aad89912..36ca638927 100644 --- a/core/src/test/kotlin/org/evomaster/core/output/RedisWriterTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/output/RedisWriterTest.kt @@ -8,37 +8,31 @@ import org.junit.jupiter.api.Test class RedisWriterTest { - private fun makeEvaluatedSet( - key: String, - value: String, - success: Boolean = true - ): EvaluatedRedisDbAction { - val action = RedisSetAction( - key = key, - valueGene = StringGene("value", value) - ) + private fun makeEvaluatedSet(key: String, value: String, success: Boolean = true): EvaluatedRedisDbAction { + val action = RedisSetAction(key = key, valueGene = StringGene("value", value)) action.setLocalId("test-redis-set-action") - val result = RedisDbActionResult(action.getLocalId()).also { - it.setInsertExecutionResult(success) - } + val result = RedisDbActionResult(action.getLocalId()).also { it.setInsertExecutionResult(success) } return EvaluatedRedisDbAction(action, result) } - private fun makeEvaluatedHset( - key: String, - field: String, - value: String, - success: Boolean = true - ): EvaluatedRedisDbAction { - val action = RedisHsetAction( - key = key, - field = field, - valueGene = StringGene("value", value) - ) + private fun makeEvaluatedHset(key: String, field: String, value: String, success: Boolean = true): EvaluatedRedisDbAction { + val action = RedisHsetAction(key = key, field = field, valueGene = StringGene("value", value)) action.setLocalId("test-redis-hset-action") - val result = RedisDbActionResult(action.getLocalId()).also { - it.setInsertExecutionResult(success) - } + val result = RedisDbActionResult(action.getLocalId()).also { it.setInsertExecutionResult(success) } + return EvaluatedRedisDbAction(action, result) + } + + private fun makeEvaluatedSadd(key: String, member: String, success: Boolean = true): EvaluatedRedisDbAction { + val action = RedisSaddAction(key = key, memberGene = StringGene("member", member)) + action.setLocalId("test-redis-sadd-action") + val result = RedisDbActionResult(action.getLocalId()).also { it.setInsertExecutionResult(success) } + return EvaluatedRedisDbAction(action, result) + } + + private fun makeEvaluatedSaddFromSinter(keys: List, member: String, success: Boolean = true): EvaluatedRedisDbAction { + val action = RedisSaddFromSinterAction(keys = keys, memberGene = StringGene("member", member)) + action.setLocalId("test-redis-sadd-sinter-action") + val result = RedisDbActionResult(action.getLocalId()).also { it.setInsertExecutionResult(success) } return EvaluatedRedisDbAction(action, result) } @@ -49,14 +43,7 @@ class RedisWriterTest { groupIndex: String = "" ): String { val lines = Lines(OutputFormat.KOTLIN_JUNIT_5) - RedisWriter.handleRedisDbInitialization( - format = OutputFormat.KOTLIN_JUNIT_5, - redisDbInitialization = actions, - lines = lines, - groupIndex = groupIndex, - insertionVars = insertionVars, - skipFailure = skipFailure - ) + RedisWriter.handleRedisDbInitialization(OutputFormat.KOTLIN_JUNIT_5, actions, lines, groupIndex, insertionVars, skipFailure) return lines.toString() } @@ -66,13 +53,7 @@ class RedisWriterTest { skipFailure: Boolean = false ): String { val lines = Lines(OutputFormat.JAVA_JUNIT_5) - RedisWriter.handleRedisDbInitialization( - format = OutputFormat.JAVA_JUNIT_5, - redisDbInitialization = actions, - lines = lines, - insertionVars = insertionVars, - skipFailure = skipFailure - ) + RedisWriter.handleRedisDbInitialization(OutputFormat.JAVA_JUNIT_5, actions, lines, "", insertionVars, skipFailure) return lines.toString() } @@ -90,7 +71,6 @@ class RedisWriterTest { @Test fun testKotlinFormatSingleSetAction() { val output = writeKotlin(listOf(makeEvaluatedSet("user:1", "Alice"))) - assertTrue(output.contains("val insertions_redis = redis()")) assertTrue(output.contains(""".set("user:1", "Alice")""")) assertTrue(output.contains(".dtos()")) @@ -100,22 +80,33 @@ class RedisWriterTest { @Test fun testKotlinFormatSingleHsetAction() { val output = writeKotlin(listOf(makeEvaluatedHset("user:1", "name", "Alice"))) - - assertTrue(output.contains("val insertions_redis = redis()")) 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 testJavaFormatSingleHsetAction() { val output = writeJava(listOf(makeEvaluatedHset("user:1", "name", "Alice"))) - assertTrue(output.contains("List insertions_redis = redis()")) assertTrue(output.contains(""".hset("user:1", "name", "Alice")""")) assertTrue(output.contains("RedisInsertionResultsDto insertions_redis_result = controller.execInsertionsIntoRedisDatabase(insertions_redis)")) } + @Test + fun testKotlinFormatSingleSaddAction() { + val output = writeKotlin(listOf(makeEvaluatedSadd("myset", "item1"))) + assertTrue(output.contains(""".sadd("myset", "item1")""")) + } + + @Test + fun testSaddFromSinterGeneratesOneSaddPerKey() { + val output = writeKotlin(listOf(makeEvaluatedSaddFromSinter(listOf("set1", "set2", "set3"), "shared"))) + + assertTrue(output.contains(""".sadd("set1", "shared")""")) + assertTrue(output.contains(""".sadd("set2", "shared")""")) + assertTrue(output.contains(""".sadd("set3", "shared")""")) + assertEquals(2, output.split(".and()").size - 1) + } + @Test fun testSkipFailureOmitsFailedActions() { val actions = listOf( @@ -123,41 +114,36 @@ class RedisWriterTest { makeEvaluatedSet("k2", "v2", success = false) ) val output = writeKotlin(actions, skipFailure = true) - assertTrue(output.contains("k1")) assertFalse(output.contains("k2")) } @Test fun testMultipleActionsUsesAndChaining() { - val actions = listOf( - makeEvaluatedSet("key:1", "val1"), - makeEvaluatedSet("key:2", "val2") - ) + val actions = listOf(makeEvaluatedSet("k1", "v1"), makeEvaluatedSet("k2", "v2")) val output = writeKotlin(actions) - assertTrue(output.contains(".and()")) - assertTrue(output.contains("key:1")) - assertTrue(output.contains("key:2")) } @Test - fun testMixedSetAndHsetActions() { + fun testMixedActions() { val actions = listOf( makeEvaluatedSet("string:key", "val"), - makeEvaluatedHset("hash:key", "field1", "val2") + makeEvaluatedHset("hash:key", "field1", "val2"), + makeEvaluatedSadd("set:key", "member"), + makeEvaluatedSaddFromSinter(listOf("s1", "s2"), "shared") ) val output = writeKotlin(actions) - assertTrue(output.contains(""".set("string:key", "val")""")) - assertTrue(output.contains(".and()")) assertTrue(output.contains(""".hset("hash:key", "field1", "val2")""")) + assertTrue(output.contains(""".sadd("set:key", "member")""")) + assertTrue(output.contains(""".sadd("s1", "shared")""")) + assertTrue(output.contains(""".sadd("s2", "shared")""")) } @Test fun testKotlinEscapesDollarSign() { val output = writeKotlin(listOf(makeEvaluatedSet("key", "\$HOME"))) - assertTrue(output.contains("\\${'$'}HOME") || output.contains("\\\$HOME")) assertFalse(output.contains("\"\$HOME\"")) } @@ -165,16 +151,12 @@ class RedisWriterTest { @Test fun testKeyWithSpecialCharactersIsEscaped() { val output = writeKotlin(listOf(makeEvaluatedSet("key\"with\"quotes", "value"))) - assertTrue(output.contains("key\\\"with\\\"quotes")) } @Test fun testGroupIndexAppearsInVariableName() { - val output = writeKotlin( - actions = listOf(makeEvaluatedSet("key:1", "val")), - groupIndex = "_42" - ) + val output = writeKotlin(listOf(makeEvaluatedSet("key:1", "val")), groupIndex = "_42") assertTrue(output.contains("insertions_redis_42")) } 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 75d6fa56ea..3905462599 100644 --- a/core/src/test/kotlin/org/evomaster/core/redis/RedisDbActionTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/redis/RedisDbActionTest.kt @@ -4,6 +4,7 @@ import org.evomaster.core.search.gene.string.StringGene import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertInstanceOf import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows class RedisDbActionTest { @@ -68,4 +69,73 @@ class RedisDbActionTest { assertEquals("modified", copy.valueGene.value) } + // --- RedisSaddAction --- + + @Test + fun testSaddActionGetName() { + val action = RedisSaddAction(key = "myset", memberGene = StringGene("member")) + assertEquals("Redis_SADD_myset", action.getName()) + } + + @Test + fun testSaddActionGetTargetKey() { + val action = RedisSaddAction(key = "myset", memberGene = StringGene("member")) + assertEquals("myset", action.getTargetKey()) + } + + @Test + fun testSaddActionSeeTopGenesReturnsMemberGene() { + val action = RedisSaddAction(key = "myset", memberGene = StringGene("member", "item1")) + assertEquals(1, action.seeTopGenes().size) + assertInstanceOf(StringGene::class.java, action.seeTopGenes()[0]) + } + + @Test + fun testSaddActionCopyIsIndependent() { + val original = RedisSaddAction(key = "myset", memberGene = StringGene("member", "item1")) + val copy = original.copy() as RedisSaddAction + copy.memberGene.value = "item2" + + assertEquals("item1", original.memberGene.value) + assertEquals("item2", copy.memberGene.value) + } + + // --- RedisSaddFromSinterAction --- + + @Test + fun testSaddFromSinterActionGetName() { + val action = RedisSaddFromSinterAction(keys = listOf("set1", "set2"), memberGene = StringGene("member")) + assertEquals("Redis_SADD_SINTER_set1_set2", action.getName()) + } + + @Test + fun testSaddFromSinterActionGetTargetKey() { + val action = RedisSaddFromSinterAction(keys = listOf("set1", "set2"), memberGene = StringGene("member")) + assertEquals("set1,set2", action.getTargetKey()) + } + + @Test + fun testSaddFromSinterActionSeeTopGenesReturnsMemberGene() { + val action = RedisSaddFromSinterAction(keys = listOf("set1", "set2"), memberGene = StringGene("member", "shared")) + assertEquals(1, action.seeTopGenes().size) + assertInstanceOf(StringGene::class.java, action.seeTopGenes()[0]) + } + + @Test + fun testSaddFromSinterActionCopyIsIndependent() { + val original = RedisSaddFromSinterAction(keys = listOf("set1", "set2"), memberGene = StringGene("member", "shared")) + val copy = original.copy() as RedisSaddFromSinterAction + copy.memberGene.value = "changed" + + assertEquals("shared", original.memberGene.value) + assertEquals("changed", copy.memberGene.value) + assertEquals(listOf("set1", "set2"), copy.keys) + } + + @Test + fun testSaddFromSinterRequiresNonEmptyKeys() { + assertThrows { + RedisSaddFromSinterAction(keys = emptyList(), memberGene = StringGene("member")) + } + } } \ 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 c22e1902b7..506dbc295b 100644 --- a/core/src/test/kotlin/org/evomaster/core/redis/RedisDbActionTransformerTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/redis/RedisDbActionTransformerTest.kt @@ -22,8 +22,7 @@ class RedisDbActionTransformerTest { @Test fun testTransformHsetAction() { - val action = - RedisHsetAction(key = "user:1", field = "name", valueGene = StringGene("value", "Alice")) + val action = RedisHsetAction(key = "user:1", field = "name", valueGene = StringGene("value", "Alice")) val dto = RedisDbActionTransformer.transform(listOf(action)) assertEquals(1, dto.insertions.size) @@ -51,6 +50,33 @@ class RedisDbActionTransformerTest { } } + @Test + fun testTransformSaddAction() { + val action = RedisSaddAction(key = "myset", memberGene = StringGene("member", "item1")) + val dto = RedisDbActionTransformer.transform(listOf(action)) + + assertEquals(1, dto.insertions.size) + with(dto.insertions[0]) { + assertEquals("SADD", command) + assertEquals("myset", key) + assertEquals("item1", value) + } + } + + @Test + fun testTransformSaddFromSinterExpandsToOneInsertionPerKey() { + val action = RedisSaddFromSinterAction( + keys = listOf("set1", "set2", "set3"), + memberGene = StringGene("member", "shared") + ) + val dto = RedisDbActionTransformer.transform(listOf(action)) + + assertEquals(3, dto.insertions.size) + dto.insertions.forEach { assertEquals("SADD", it.command) } + dto.insertions.forEach { assertEquals("shared", it.value) } + assertEquals(setOf("set1", "set2", "set3"), dto.insertions.map { it.key }.toSet()) + } + @Test fun testTransformMixedActions() { val actions = listOf( 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 9c0b2d8345..f567f13681 100644 --- a/core/src/test/kotlin/org/evomaster/core/redis/RedisInsertBuilderTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/redis/RedisInsertBuilderTest.kt @@ -10,30 +10,38 @@ class RedisInsertBuilderTest { // --- GET --- @Test - fun testGetBuildsSetAction() { - val actions = RedisInsertBuilder.buildInsertActions( - listOf(getCommand("user:1")), emptySet() - ) - assertEquals(1, actions.size) + fun testGetBuildsSetActionForEachCommand() { + val commands = listOf(getCommand("user:1"), getCommand("user:2")) + val actions = RedisInsertBuilder.buildInsertActions(commands, emptySet()) + + assertEquals(2, actions.size) + actions.forEach { assertTrue(it is RedisSetAction) } assertEquals("user:1", (actions[0] as RedisSetAction).key) + assertEquals("user:2", (actions[1] as RedisSetAction).key) } @Test - fun testGetSkipsExistingKey() { - val actions = RedisInsertBuilder.buildInsertActions( - listOf(getCommand("user:1"), getCommand("user:2")), setOf("user:1") - ) + fun testGetSkipsExistingKeys() { + val commands = listOf(getCommand("user:1"), getCommand("user:2")) + val actions = RedisInsertBuilder.buildInsertActions(commands, setOf("user:1")) + assertEquals(1, actions.size) assertEquals("user:2", (actions[0] as RedisSetAction).key) } + @Test + fun testGetKeyInitializedWithObservedKey() { + val actions = RedisInsertBuilder.buildInsertActions(listOf(getCommand("known:key")), emptySet()) + assertEquals("known:key", (actions[0] as RedisSetAction).key) + } + // --- HGET --- @Test - fun testHgetBuildsHsetActionWithObservedField() { - val actions = RedisInsertBuilder.buildInsertActions( - listOf(hgetCommand("user:1", "name")), emptySet() - ) + fun testHgetBuildsHsetAction() { + val actions = RedisInsertBuilder.buildInsertActions(listOf(hgetCommand("user:1", "name")), emptySet()) + + assertEquals(1, actions.size) val action = actions[0] as RedisHsetAction assertEquals("user:1", action.key) assertEquals("name", action.field) @@ -43,9 +51,9 @@ class RedisInsertBuilderTest { @Test fun testHgetallBuildsHsetActionWithPlaceholderField() { - val actions = RedisInsertBuilder.buildInsertActions( - listOf(hgetallCommand("user:1")), emptySet() - ) + val actions = RedisInsertBuilder.buildInsertActions(listOf(hgetallCommand("user:1")), emptySet()) + + assertEquals(1, actions.size) val action = actions[0] as RedisHsetAction assertEquals("user:1", action.key) assertEquals("field", action.field) @@ -55,19 +63,69 @@ class RedisInsertBuilderTest { @Test fun testKeysBuildsSetFromPatternAction() { - val actions = RedisInsertBuilder.buildInsertActions( - listOf(keysCommand("^user:.*$")), emptySet() - ) + val actions = RedisInsertBuilder.buildInsertActions(listOf(keysCommand("^user:.*$")), emptySet()) + assertEquals(1, actions.size) assertInstanceOf(RedisSetFromPatternAction::class.java, actions[0]) assertEquals("^user:.*$", (actions[0] as RedisSetFromPatternAction).keyGene.sourceRegex) } + // --- SMEMBERS --- + @Test - fun testSkipsAllIfAllExist() { + fun testSmembersBuildsRedisSaddAction() { + val actions = RedisInsertBuilder.buildInsertActions(listOf(smembersCommand("myset")), emptySet()) + + assertEquals(1, actions.size) + assertEquals("myset", (actions[0] as RedisSaddAction).key) + } + + @Test + fun testSmembersSkipsExistingKey() { + val actions = RedisInsertBuilder.buildInsertActions( + listOf(smembersCommand("myset")), setOf("myset") + ) + assertTrue(actions.isEmpty()) + } + + // --- SINTER --- + + @Test + fun testSinterBuildsRedisSaddFromSinterAction() { val actions = RedisInsertBuilder.buildInsertActions( - listOf(getCommand("user:1"), getCommand("user:2")), setOf("user:1", "user:2") + listOf(sinterCommand("set1", "set2", "set3")), emptySet() ) + + assertEquals(1, actions.size) + val action = actions[0] as RedisSaddFromSinterAction + assertEquals(listOf("set1", "set2", "set3"), action.keys) + } + + @Test + fun testSinterAlwaysProcessedEvenIfKeysExist() { + val actions = RedisInsertBuilder.buildInsertActions( + listOf(sinterCommand("set1", "set2")), setOf("set1", "set2") + ) + assertEquals(1, actions.size) + assertInstanceOf(RedisSaddFromSinterAction::class.java, actions[0]) + } + + @Test + fun testSinterSharesSingleMemberGeneAcrossKeys() { + val actions = RedisInsertBuilder.buildInsertActions( + listOf(sinterCommand("set1", "set2")), emptySet() + ) + val action = actions[0] as RedisSaddFromSinterAction + assertEquals(2, action.keys.size) + assertNotNull(action.memberGene) + } + + // --- general --- + + @Test + fun testSkipsAllIfAllExist() { + val commands = listOf(getCommand("user:1"), getCommand("user:2")) + val actions = RedisInsertBuilder.buildInsertActions(commands, setOf("user:1", "user:2")) assertTrue(actions.isEmpty()) } @@ -77,20 +135,53 @@ class RedisInsertBuilderTest { } @Test - fun testUnsupportedCommandThrowsAssertionError() { - val cmd = RedisFailedCommand().also { it.key = "k"; it.command = "UNKNOWN" } + fun testEachActionHasIndependentValueGene() { + val commands = listOf(getCommand("key:1"), getCommand("key:2")) + val actions = RedisInsertBuilder.buildInsertActions(commands, emptySet()) + + (actions[0] as RedisSetAction).valueGene.value = "mutated" + assertNotEquals("mutated", (actions[1] as RedisSetAction).valueGene.value) + } + @Test + fun testUnsupportedCommandThrowsAssertionError() { + val unsupported = RedisFailedCommand().also { + it.keys = listOf("key:1") + it.command = "UNKNOWN_CMD" + } assertThrows { - RedisInsertBuilder.buildInsertActions(listOf(cmd), emptySet()) + RedisInsertBuilder.buildInsertActions(listOf(unsupported), emptySet()) } } - 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" } + private fun getCommand(key: String) = RedisFailedCommand().also { + it.keys = listOf(key) + it.command = "GET" + } + + private fun hgetCommand(key: String, field: String) = RedisFailedCommand().also { + it.keys = listOf(key) + it.field = field + it.command = "HGET" + } + + private fun hgetallCommand(key: String) = RedisFailedCommand().also { + it.keys = listOf(key) + it.command = "HGETALL" + } + + private fun keysCommand(pattern: String) = RedisFailedCommand().also { + it.pattern = pattern + it.command = "KEYS" + } + + private fun smembersCommand(key: String) = RedisFailedCommand().also { + it.keys = listOf(key) + it.command = "SMEMBERS" + } + + private fun sinterCommand(vararg keys: String) = RedisFailedCommand().also { + it.keys = keys.toList() + it.command = "SINTER" + } } \ No newline at end of file