diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/RedisValueData.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/RedisValueData.java index 2d702e093d..4aafaf3dea 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/RedisValueData.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/redis/RedisValueData.java @@ -1,5 +1,7 @@ package org.evomaster.client.java.controller.redis; +import com.fasterxml.jackson.databind.JsonNode; + import java.util.Map; import java.util.Set; @@ -9,10 +11,13 @@ * Fields or members will be set depending on the type of key. * String keys may have a String value in the future, but currently it is not needed to store that information. * Set keys will have the set members. Hash keys will have the fields. + * JSON keys will have the parsed JSON document, needed to navigate JSONPath expressions + * (e.g. JSON.GET/JSON.ARRLEN/JSON.ARRINDEX and similar commands). */ public class RedisValueData { private Map fields; private Set members; + private JsonNode jsonValue; public RedisValueData(Map fields) { this.fields = fields; @@ -22,6 +27,10 @@ public RedisValueData(Set members) { this.members = members; } + public RedisValueData(JsonNode jsonValue) { + this.jsonValue = jsonValue; + } + public Set getMembers() { return members; @@ -30,4 +39,8 @@ public Set getMembers() { public Map getFields() { return fields; } -} + + public JsonNode getJsonValue() { + return jsonValue; + } +} \ No newline at end of file diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/redis/RedisHandlerIntegrationTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/redis/RedisHandlerIntegrationTest.java index ee451f3409..08d1c32ab4 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/redis/RedisHandlerIntegrationTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/redis/RedisHandlerIntegrationTest.java @@ -51,13 +51,13 @@ void testHeuristicDistanceForStringExists() { RedisCommand similarKeyCmd = new RedisCommand( RedisCommand.RedisCommandType.EXISTS, - new String[]{"key"}, + new String[]{"user:3"}, true, 10 ); RedisCommand differentKeyCmd = new RedisCommand( RedisCommand.RedisCommandType.EXISTS, - new String[]{"key"}, + new String[]{"user:82bd3bff-4567-40f4-a42e-27f87276199f"}, true, 10 ); @@ -94,7 +94,7 @@ void testHeuristicDistanceForStringExists() { void testResetClearsCommands() { RedisCommand cmd = new RedisCommand( RedisCommand.RedisCommandType.EXISTS, - new String[]{"key"}, + new String[]{"user:1"}, true, 5 ); diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/redis/RedisHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/redis/RedisHeuristicsCalculatorTest.java index 3fa3bcb08a..a272478297 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/redis/RedisHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/redis/RedisHeuristicsCalculatorTest.java @@ -26,7 +26,7 @@ void setup() { void testKeysPatternExactMatch() { RedisCommand cmd = new RedisCommand( RedisCommand.RedisCommandType.KEYS, - new String[]{"key"}, + new String[]{"user*"}, true, 5 ); @@ -46,7 +46,7 @@ void testKeysPatternExactMatch() { void testKeysPatternNoMatch() { RedisCommand cmd = new RedisCommand( RedisCommand.RedisCommandType.KEYS, - new String[]{"key"}, + new String[]{"thiskeydoesnotexist*"}, true, 5 ); @@ -65,14 +65,14 @@ void testKeysPatternNoMatch() { void testExistsCommandSimilarity() { RedisCommand closeKey = new RedisCommand( RedisCommand.RedisCommandType.EXISTS, - new String[]{"key"}, + new String[]{"user:3"}, true, 5 ); RedisCommand farKey = new RedisCommand( RedisCommand.RedisCommandType.EXISTS, - new String[]{"key"}, + new String[]{"abcxyz"}, true, 5 ); @@ -93,7 +93,7 @@ void testExistsCommandSimilarity() { void testHGetFieldExists() { RedisCommand cmd = new RedisCommand( RedisCommand.RedisCommandType.HGET, - new String[]{"key", "key"}, + new String[]{"profile", "name"}, true, 3 ); @@ -114,7 +114,7 @@ void testHGetFieldExists() { void testHGetFieldNotExists() { RedisCommand cmd = new RedisCommand( RedisCommand.RedisCommandType.HGET, - new String[]{"key", "key"}, + new String[]{"profile", "age"}, true, 3 ); @@ -133,19 +133,19 @@ void testHGetFieldNotExists() { void testHGetFieldDistance() { RedisCommand lowerDistanceCmd = new RedisCommand( RedisCommand.RedisCommandType.HGET, - new String[]{"key", "key"}, + new String[]{"profile", "weight"}, true, 3 ); RedisCommand cmd = new RedisCommand( RedisCommand.RedisCommandType.HGET, - new String[]{"key", "key"}, + new String[]{"profile", "age"}, true, 3 ); RedisCommand greaterDistanceCmd = new RedisCommand( RedisCommand.RedisCommandType.HGET, - new String[]{"key", "key"}, + new String[]{"user", "direction"}, true, 3 ); @@ -168,7 +168,7 @@ void testHGetFieldDistance() { void testSInterSetsIntersectionAndNoIntersection() { RedisCommand cmdIntersect = new RedisCommand( RedisCommand.RedisCommandType.SINTER, - new String[]{"key", "key"}, + new String[]{"setA", "setB"}, true, 1 ); @@ -184,7 +184,7 @@ void testSInterSetsIntersectionAndNoIntersection() { RedisCommand cmdNoIntersect = new RedisCommand( RedisCommand.RedisCommandType.SINTER, - new String[]{"key", "key"}, + new String[]{"setC", "setD"}, true, 1 ); @@ -203,7 +203,7 @@ void testSInterSetsIntersectionAndNoIntersection() { RedisCommand cmdNoIntersectFarDistance = new RedisCommand( RedisCommand.RedisCommandType.SINTER, - new String[]{"key", "key"}, + new String[]{"setE", "setF"}, true, 1 ); @@ -225,7 +225,7 @@ void testSInterSetsIntersectionAndNoIntersection() { void testSInterSeveralSets() { RedisCommand cmdIntersect = new RedisCommand( RedisCommand.RedisCommandType.SINTER, - new String[]{"key", "key", "key", "key"}, + new String[]{"setA", "setB", "setC", "setD"}, true, 1 ); @@ -260,13 +260,13 @@ void testSInterSeveralSets() { void testSMembersSimilarity() { RedisCommand similar = new RedisCommand( RedisCommand.RedisCommandType.SMEMBERS, - new String[]{"key"}, + new String[]{"user:set1"}, true, 2 ); RedisCommand different = new RedisCommand( RedisCommand.RedisCommandType.SMEMBERS, - new String[]{"key"}, + new String[]{"orders"}, true, 2 ); @@ -288,14 +288,14 @@ void testSMembersSimilarity() { void testGetCommandSimilarity() { RedisCommand similar = new RedisCommand( RedisCommand.RedisCommandType.GET, - new String[]{"key"}, + new String[]{"session:1234"}, true, 1 ); RedisCommand different = new RedisCommand( RedisCommand.RedisCommandType.GET, - new String[]{"key"}, + new String[]{"orders"}, true, 1 ); @@ -317,7 +317,7 @@ void testGetCommandSimilarity() { void testComputeDistanceHandlesInternalExceptionOk() { RedisCommand malformedHGet = new RedisCommand( RedisCommand.RedisCommandType.HGET, - new String[]{"key"}, + new String[]{"profile"}, true, 3 ); @@ -338,7 +338,7 @@ void testComputeDistanceHandlesInternalExceptionOk() { void testUnsupportedCommandTypeReturnsMaxDistance() { RedisCommand unsupported = new RedisCommand( RedisCommand.RedisCommandType.SET, - new String[]{"key", "key"}, + new String[]{"foo", "bar"}, true, 1 ); @@ -358,7 +358,7 @@ void testUnsupportedCommandTypeReturnsMaxDistance() { void testHGetAllCommand() { RedisCommand cmd = new RedisCommand( RedisCommand.RedisCommandType.HGETALL, - new String[]{"key"}, + new String[]{"profile"}, true, 1 ); @@ -376,7 +376,7 @@ void testHGetAllCommand() { void testKeyMatchAgainstEmptyDatabase() { RedisCommand cmd = new RedisCommand( RedisCommand.RedisCommandType.GET, - new String[]{"key"}, + new String[]{"anykey"}, true, 1 ); @@ -394,7 +394,7 @@ void testKeyMatchAgainstEmptyDatabase() { void testKeysInvalidPatternIsHandledOk() { RedisCommand cmd = new RedisCommand( RedisCommand.RedisCommandType.KEYS, - new String[]{"key<[abc>"}, + new String[]{"[abc"}, true, 1 ); @@ -416,7 +416,7 @@ void testKeysInvalidPatternIsHandledOk() { void testKeysAgainstEmptyDatabase() { RedisCommand cmd = new RedisCommand( RedisCommand.RedisCommandType.KEYS, - new String[]{"key"}, + new String[]{"user*"}, true, 1 ); @@ -453,7 +453,7 @@ void testSInterWithNoKeysReturnsMaxDistance() { void testSInterAgainstEmptyDatabase() { RedisCommand cmd = new RedisCommand( RedisCommand.RedisCommandType.SINTER, - new String[]{"key", "key"}, + new String[]{"setA", "setB"}, true, 1 ); @@ -471,7 +471,7 @@ void testSInterAgainstEmptyDatabase() { void testSInterWithMissingSetKeyReturnsMaxDistance() { RedisCommand cmd = new RedisCommand( RedisCommand.RedisCommandType.SINTER, - new String[]{"key", "key"}, + new String[]{"setA", "setB"}, true, 1 ); @@ -490,7 +490,7 @@ void testSInterWithMissingSetKeyReturnsMaxDistance() { void testSInterWithAllEmptySetsReturnsMaxDistance() { RedisCommand cmd = new RedisCommand( RedisCommand.RedisCommandType.SINTER, - new String[]{"key", "key"}, + new String[]{"setA", "setB"}, true, 1 ); @@ -510,7 +510,7 @@ void testSInterWithAllEmptySetsReturnsMaxDistance() { void testSInterOneEmptySetAmongNonEmptySetsDoesNotThrow() { RedisCommand cmd = new RedisCommand( RedisCommand.RedisCommandType.SINTER, - new String[]{"key", "key"}, + new String[]{"setA", "setB"}, true, 1 ); diff --git a/client-java/instrumentation/pom.xml b/client-java/instrumentation/pom.xml index c90d0a4e4e..bac75c5cbd 100644 --- a/client-java/instrumentation/pom.xml +++ b/client-java/instrumentation/pom.xml @@ -67,6 +67,11 @@ ${springboot.version} test + + redis.clients + jedis + test + org.springframework.boot spring-boot-starter-data-redis diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/RedisCommand.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/RedisCommand.java index d836bed6aa..9e1880046c 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/RedisCommand.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/RedisCommand.java @@ -1,7 +1,6 @@ package org.evomaster.client.java.instrumentation; import java.io.Serializable; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -37,6 +36,16 @@ public enum RedisCommandType { * EXISTS Documentation */ EXISTS("exists", "mixed", true), + /** + * Runs a search query on an index and performs aggregate transformations on the results. + * FT.AGGREGATE Documentation + */ + FT_AGGREGATE("ft.aggregate", "search", true), + /** + * Searches the index with a textual query, returning either documents or just ids. + * FT.SEARCH Documentation + */ + FT_SEARCH("ft.search", "search", true), /** * Get the value of key. * GET Documentation @@ -52,6 +61,13 @@ public enum RedisCommandType { * HGETALL Documentation */ HGETALL("hgetall", "hash", true), + /** + * Sets the specified fields to their respective values in the hash stored at key. + * This command overwrites the values of specified fields that exist in the hash. + * If key doesn't exist, a new key holding a hash is created. + * HSET Documentation + */ + HSET("hset", "hash", false), /** * Increments the number stored at key by one. * If the key does not exist, it is set to 0 before performing the operation. @@ -60,13 +76,6 @@ public enum RedisCommandType { * This operation is limited to 64-bit signed integers. * INCR Documentation */ - HSET("hset", "hash", false), - /** - * Sets the specified fields to their respective values in the hash stored at key. - * This command overwrites the values of specified fields that exist in the hash. - * If key doesn't exist, a new key holding a hash is created. - * HSET Documentation - */ INCR("incr", "string", false), /** * Returns all keys matching pattern. @@ -181,8 +190,7 @@ public boolean shouldCalculateHeuristic() { private final RedisCommandType type; /** - * Keys or values used in query. Keys are used in most queries. Values are used in Set commands. - * Keys are wrapped in {@literal key<...>} while values in {@literal value<...>} + * Already-parsed argument values, in the order the command received them. */ private final String[] args; @@ -215,11 +223,7 @@ public String[] getArgs() { } public List extractArgs(){ - List parameters = new ArrayList<>(); - for(String arg : args){ - parameters.add(arg.substring(arg.indexOf('<')+1, arg.indexOf('>'))); - } - return parameters; + return Arrays.asList(args); } public boolean getSuccessfullyExecuted() { diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/ReplacementList.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/ReplacementList.java index 24f2b9d3e2..4be1c6b1fd 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/ReplacementList.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/ReplacementList.java @@ -31,6 +31,7 @@ public static List getList() { new ByteClassReplacement(), new CharacterClassReplacement(), new CollectionClassReplacement(), + new ConnectionClassReplacement(), new CqlSessionClassReplacement(), new CursorPreparerClassReplacement(), new DateClassReplacement(), diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/ConnectionClassReplacement.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/ConnectionClassReplacement.java new file mode 100644 index 0000000000..0001aa5262 --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/ConnectionClassReplacement.java @@ -0,0 +1,93 @@ +package org.evomaster.client.java.instrumentation.coverage.methodreplacement.thirdpartyclasses; + +import org.evomaster.client.java.instrumentation.RedisCommand; +import org.evomaster.client.java.instrumentation.coverage.methodreplacement.Replacement; +import org.evomaster.client.java.instrumentation.coverage.methodreplacement.ThirdPartyCast; +import org.evomaster.client.java.instrumentation.coverage.methodreplacement.ThirdPartyMethodReplacementClass; +import org.evomaster.client.java.instrumentation.coverage.methodreplacement.UsageFilter; +import org.evomaster.client.java.instrumentation.shared.ReplacementCategory; +import org.evomaster.client.java.instrumentation.shared.ReplacementType; +import org.evomaster.client.java.instrumentation.staticstate.ExecutionTracer; +import org.evomaster.client.java.utils.SimpleLogger; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * This replacement captures Redis command execution through Jedis. + */ +public class ConnectionClassReplacement extends ThirdPartyMethodReplacementClass { + + private static final ConnectionClassReplacement singleton = new ConnectionClassReplacement(); + + private static final String EXECUTE_COMMAND = "executeCommand"; + + @Override + protected String getNameOfThirdPartyTargetClass() { + return "redis.clients.jedis.Connection"; + } + + @Replacement(replacingStatic = false, type = ReplacementType.TRACKER, id = EXECUTE_COMMAND, + usageFilter = UsageFilter.ANY, category = ReplacementCategory.REDIS) + public static Object executeCommand(Object connection, @ThirdPartyCast(actualType = "redis.clients.jedis.CommandObject") Object commandObject) { + try { + long start = System.currentTimeMillis(); + + Method m = getOriginal(singleton, EXECUTE_COMMAND, connection); + Object result = m.invoke(connection, commandObject); + + long end = System.currentTimeMillis(); + + try { + recordCommand(commandObject, end - start); + } catch (Exception e) { + SimpleLogger.uniqueWarn("Failed to record Redis command captured via Connection.executeCommand: " + e.getMessage()); + } + + return result; + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e.getCause()); + } + } + + private static void recordCommand(Object commandObject, long executionTime) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { + Object commandArguments = commandObject.getClass().getMethod("getArguments").invoke(commandObject); + Object protocolCommand = commandArguments.getClass().getMethod("getCommand").invoke(commandArguments); + byte[] rawCommand = (byte[]) protocolCommand.getClass().getMethod("getRaw").invoke(protocolCommand); + String commandName = new String(rawCommand, StandardCharsets.US_ASCII).toUpperCase().replace('.', '_'); + + RedisCommand.RedisCommandType type; + try { + type = RedisCommand.RedisCommandType.valueOf(commandName); + } catch (IllegalArgumentException e) { + type = RedisCommand.RedisCommandType.OTHER; + } + + String[] args = extractArgs(commandArguments); + + RedisCommand cmd = new RedisCommand(type, args, true, executionTime); + ExecutionTracer.addRedisCommand(cmd); + } + + /** + * CommandArguments stores the command keyword itself as the first element + * of the same list it iterates over - already captured as RedisCommandType. + */ + private static String[] extractArgs(Object commandArguments) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { + Iterator iterator = (Iterator) commandArguments.getClass().getMethod("iterator").invoke(commandArguments); + List args = new ArrayList<>(); + if (iterator.hasNext()) { + iterator.next(); + } + while (iterator.hasNext()) { + Object rawable = iterator.next(); + byte[] raw = (byte[]) rawable.getClass().getMethod("getRaw").invoke(rawable); + args.add(new String(raw, StandardCharsets.US_ASCII)); + } + return args.toArray(new String[0]); + } +} diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/StatefulConnectionClassReplacement.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/StatefulConnectionClassReplacement.java index ed28172868..e1aa7c627d 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/StatefulConnectionClassReplacement.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/StatefulConnectionClassReplacement.java @@ -11,6 +11,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.List; /** * This replacement captures Redis dispatch operations containing Redis Commands. @@ -49,10 +50,8 @@ public static Object dispatch(Object redis, @ThirdPartyCast(actualType = "io.let Method argsMethod = command.getClass().getMethod("getArgs"); Object commandArgs = argsMethod.invoke(command); - Method toCmdString = commandArgs.getClass().getMethod("toCommandString"); - String fullCmd = (String) toCmdString.invoke(commandArgs); - String[] args = fullCmd.trim().split("\\s+"); + String[] args = parseArgs(commandArgs); RedisCommand.RedisCommandType cmdType; try { @@ -71,6 +70,34 @@ public static Object dispatch(Object redis, @ThirdPartyCast(actualType = "io.let } } + /** + * Reads CommandArgs's private singularArguments field directly instead of + * calling its public toCommandString(), which joins every argument into a + * single space-separated string - unrecoverable if any argument value + * itself contains a space. Walking the list lets each argument be read on + * its own via toString(), which Lettuce's KeyArgument/ValueArgument render + * as "key<...>"/"value<...>" (unwrapped below); every other argument type + * renders as its plain value. + */ + private static String[] parseArgs(Object commandArgs) { + List singularArguments = (List) getField(commandArgs, "singularArguments"); + String[] args = new String[singularArguments.size()]; + for (int i = 0; i < singularArguments.size(); i++) { + args[i] = unwrapArg(singularArguments.get(i).toString()); + } + return args; + } + + private static String unwrapArg(String token) { + if (token.startsWith("key<") && token.endsWith(">")) { + return token.substring(4, token.length() - 1); + } + if (token.startsWith("value<") && token.endsWith(">")) { + return token.substring(6, token.length() - 1); + } + return token; + } + private static void addRedisCommand(RedisCommand.RedisCommandType type, String[] args, long executionTime) { RedisCommand cmd = new RedisCommand( type, diff --git a/client-java/instrumentation/src/test/java/com/foo/somedifferentpackage/examples/methodreplacement/redis/JedisOperationsImpl.java b/client-java/instrumentation/src/test/java/com/foo/somedifferentpackage/examples/methodreplacement/redis/JedisOperationsImpl.java new file mode 100644 index 0000000000..165fdf8f91 --- /dev/null +++ b/client-java/instrumentation/src/test/java/com/foo/somedifferentpackage/examples/methodreplacement/redis/JedisOperationsImpl.java @@ -0,0 +1,44 @@ +package com.foo.somedifferentpackage.examples.methodreplacement.redis; + +import org.evomaster.client.java.instrumentation.example.redis.JedisOperations; +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.UnifiedJedis; +import redis.clients.jedis.search.FTCreateParams; +import redis.clients.jedis.search.schemafields.SchemaField; +import redis.clients.jedis.search.schemafields.TextField; + +import java.util.Collections; + +/** + * {@link JedisOperations} implementation backed by a real {@link UnifiedJedis} connection, + * used as the instrumentation target in JedisInstrumentedTest. + */ +public class JedisOperationsImpl implements JedisOperations { + + private final UnifiedJedis jedis; + + public JedisOperationsImpl(String host, int port) { + this.jedis = new UnifiedJedis(new HostAndPort(host, port)); + } + + @Override + public String get(String key) { + return jedis.get(key); + } + + @Override + public void ftCreate(String index, String prefix, String textField) { + FTCreateParams params = FTCreateParams.createParams().prefix(prefix); + jedis.ftCreate(index, params, Collections.singletonList(TextField.of(textField))); + } + + @Override + public void hset(String key, String field, String value) { + jedis.hset(key, field, value); + } + + @Override + public long ftSearch(String index, String query) { + return jedis.ftSearch(index, query).getTotalResults(); + } +} diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/ConnectionClassReplacementTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/ConnectionClassReplacementTest.java new file mode 100644 index 0000000000..3cdd763eb1 --- /dev/null +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/ConnectionClassReplacementTest.java @@ -0,0 +1,91 @@ +package org.evomaster.client.java.instrumentation.coverage.methodreplacement.thirdpartyclasses; + +import org.evomaster.client.java.instrumentation.AdditionalInfo; +import org.evomaster.client.java.instrumentation.staticstate.ExecutionTracer; +import org.junit.jupiter.api.*; +import redis.clients.jedis.CommandArguments; +import redis.clients.jedis.CommandObject; +import redis.clients.jedis.Connection; +import redis.clients.jedis.Protocol; +import redis.clients.jedis.search.SearchProtocol; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +public class ConnectionClassReplacementTest { + + private Connection mockConnection; + + private static final String GET = "GET"; + + @BeforeEach + public void setup() { + ExecutionTracer.reset(); + mockConnection = mock(Connection.class); + } + + @Test + public void testExecuteCommandCoreGet() { + String key = "foo"; + CommandArguments args = new CommandArguments(Protocol.Command.GET).key(key); + CommandObject commandObject = new CommandObject<>(args, null); + + ConnectionClassReplacement.executeCommand(mockConnection, commandObject); + + List infoList = ExecutionTracer.exposeAdditionalInfoList(); + assertEquals(1, infoList.size()); + + org.evomaster.client.java.instrumentation.RedisCommand redisCmd = + infoList.get(0).getRedisCommandData().iterator().next(); + + assertEquals(GET, redisCmd.getType().name()); + assertArrayEquals(new String[]{key}, redisCmd.getArgs()); + } + + @Test + public void testExecuteCommandFtAggregate() { + String indexName = "myIndex"; + String query = "*"; + CommandArguments args = new CommandArguments(SearchProtocol.SearchCommand.AGGREGATE) + .add(indexName) + .add(query) + .add("GROUPBY") + .add("1") + .add("@category"); + CommandObject commandObject = new CommandObject<>(args, null); + + ConnectionClassReplacement.executeCommand(mockConnection, commandObject); + + List infoList = ExecutionTracer.exposeAdditionalInfoList(); + assertEquals(1, infoList.size()); + + org.evomaster.client.java.instrumentation.RedisCommand redisCmd = + infoList.get(0).getRedisCommandData().iterator().next(); + + assertEquals("FT_AGGREGATE", redisCmd.getType().name()); + assertArrayEquals(new String[]{indexName, query, "GROUPBY", "1", "@category"}, redisCmd.getArgs()); + } + + @Test + public void testExecuteCommandFtSearch() { + String indexName = "myIndex"; + String query = "@title:redis"; + CommandArguments args = new CommandArguments(SearchProtocol.SearchCommand.SEARCH) + .add(indexName) + .add(query); + CommandObject commandObject = new CommandObject<>(args, null); + + ConnectionClassReplacement.executeCommand(mockConnection, commandObject); + + List infoList = ExecutionTracer.exposeAdditionalInfoList(); + assertEquals(1, infoList.size()); + + org.evomaster.client.java.instrumentation.RedisCommand redisCmd = + infoList.get(0).getRedisCommandData().iterator().next(); + + assertEquals("FT_SEARCH", redisCmd.getType().name()); + assertArrayEquals(new String[]{indexName, query}, redisCmd.getArgs()); + } +} diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/StatefulConnectionClassReplacementTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/StatefulConnectionClassReplacementTest.java index 2b57fc52d6..ec9f4bb8ea 100644 --- a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/StatefulConnectionClassReplacementTest.java +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/StatefulConnectionClassReplacementTest.java @@ -45,7 +45,7 @@ public void testDispatchGet() { org.evomaster.client.java.instrumentation.RedisCommand redisCmd = infoList.get(0).getRedisCommandData().iterator().next(); assertEquals(GET, redisCmd.getType().name()); - assertArrayEquals(new String[]{createKeyArg(key)}, redisCmd.getArgs()); + assertArrayEquals(new String[]{key}, redisCmd.getArgs()); } @Test @@ -66,13 +66,6 @@ public void testDispatchHGet() { org.evomaster.client.java.instrumentation.RedisCommand redisCmd = infoList.get(0).getRedisCommandData().iterator().next(); assertEquals(HGET, redisCmd.getType().name()); - assertArrayEquals(new String[]{ - createKeyArg(key), - createKeyArg(field) - }, redisCmd.getArgs()); - } - - private String createKeyArg(String key) { - return "key<" + key + ">"; + assertArrayEquals(new String[]{key, field}, redisCmd.getArgs()); } } \ No newline at end of file diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/example/redis/JedisInstrumentedTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/example/redis/JedisInstrumentedTest.java new file mode 100644 index 0000000000..d487033988 --- /dev/null +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/example/redis/JedisInstrumentedTest.java @@ -0,0 +1,94 @@ +package org.evomaster.client.java.instrumentation.example.redis; + +import com.foo.somedifferentpackage.examples.methodreplacement.redis.JedisOperationsImpl; +import org.evomaster.client.java.instrumentation.AdditionalInfo; +import org.evomaster.client.java.instrumentation.InputProperties; +import org.evomaster.client.java.instrumentation.InstrumentingClassLoader; +import org.evomaster.client.java.instrumentation.staticstate.ExecutionTracer; +import org.junit.jupiter.api.*; +import org.testcontainers.containers.GenericContainer; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class JedisInstrumentedTest { + + private static String defaultReplacement; + + private static final int REDIS_PORT = 6379; + + // redis-stack-server, not plain redis, so the RediSearch module is loaded + // server-side and FT.CREATE/FT.SEARCH don't error out + private static final GenericContainer redisContainer = + new GenericContainer<>("redis/redis-stack-server:latest") + .withExposedPorts(REDIS_PORT); + + private static final String GET = "GET"; + private static final String FT_SEARCH = "FT_SEARCH"; + + @BeforeAll + public static void setupAll() { + redisContainer.start(); + + defaultReplacement = System.getProperty(InputProperties.REPLACEMENT_CATEGORIES); + if (defaultReplacement != null) { + System.setProperty(InputProperties.REPLACEMENT_CATEGORIES, defaultReplacement + ",REDIS"); + } else { + System.setProperty(InputProperties.REPLACEMENT_CATEGORIES, "BASE,SQL,EXT_0,NET,MONGO,REDIS"); + } + } + + @AfterAll + public static void teardownAll() { + redisContainer.stop(); + if (defaultReplacement != null) { + System.setProperty(InputProperties.REPLACEMENT_CATEGORIES, defaultReplacement); + } + } + + private JedisOperations getInstance() throws Exception { + InstrumentingClassLoader cl = new InstrumentingClassLoader("com.foo"); + return (JedisOperations) cl.loadClass(JedisOperationsImpl.class.getName()) + .getConstructor(String.class, int.class) + .newInstance(redisContainer.getHost(), redisContainer.getMappedPort(REDIS_PORT)); + } + + @Test + public void testGetInstrumentationWithClassLoader() throws Exception { + ExecutionTracer.reset(); + + JedisOperations jedisInstrumented = getInstance(); + jedisInstrumented.get("foo"); + + List infoList = ExecutionTracer.exposeAdditionalInfoList(); + assertFalse(infoList.isEmpty(), "Expected Redis instrumentation data"); + + boolean foundGet = infoList.stream() + .flatMap(i -> i.getRedisCommandData().stream()) + .anyMatch(cmd -> cmd.getType().name().equals(GET)); + + assertTrue(foundGet, "Expected a GET command to be instrumented via ConnectionClassReplacement"); + } + + @Test + public void testFtSearchInstrumentationWithClassLoader() throws Exception { + ExecutionTracer.reset(); + + JedisOperations jedisInstrumented = getInstance(); + jedisInstrumented.ftCreate("idx:products", "product:", "title"); + jedisInstrumented.hset("product:1", "title", "redis handbook"); + long total = jedisInstrumented.ftSearch("idx:products", "@title:redis"); + + assertEquals(1, total, "Expected the indexed document to be found by FT.SEARCH"); + + List infoList = ExecutionTracer.exposeAdditionalInfoList(); + assertFalse(infoList.isEmpty(), "Expected Redis instrumentation data"); + + boolean foundFtSearch = infoList.stream() + .flatMap(i -> i.getRedisCommandData().stream()) + .anyMatch(cmd -> cmd.getType().name().equals(FT_SEARCH)); + + assertTrue(foundFtSearch, "Expected an FT.SEARCH command to be instrumented via ConnectionClassReplacement"); + } +} diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/example/redis/JedisOperations.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/example/redis/JedisOperations.java new file mode 100644 index 0000000000..6b1b2e143e --- /dev/null +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/example/redis/JedisOperations.java @@ -0,0 +1,12 @@ +package org.evomaster.client.java.instrumentation.example.redis; + +/** + * Minimal Jedis-backed Redis client used to verify method-replacement instrumentation + * on {@link org.evomaster.client.java.instrumentation.coverage.methodreplacement.thirdpartyclasses.ConnectionClassReplacement}. + */ +public interface JedisOperations { + String get(String key); + void ftCreate(String index, String prefix, String textField); + void hset(String key, String field, String value); + long ftSearch(String index, String query); +}