Repository: fppt/jedis-mock
Pull request #118 (2021-05-08 00:53:20)

mock `hello` with hard-coded RESP2 protover to solve #104


Patch:
diff --git a/pom.xml b/pom.xml
index 05ee923..65d86f1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -77,6 +77,12 @@
             <version>${jedis.version}</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>io.lettuce</groupId>
+            <artifactId>lettuce-core</artifactId>
+            <version>6.1.1.RELEASE</version>
+            <scope>test</scope>
+        </dependency>
         <!-- Used for creating an testcontainer of Redis for testing and comparing behaviour-->
         <dependency>
             <groupId>org.testcontainers</groupId>
diff --git a/src/main/java/com/github/fppt/jedismock/operations/AbstractByScoreOperation.java b/src/main/java/com/github/fppt/jedismock/operations/AbstractByScoreOperation.java
new file mode 100644
index 0000000..af795e9
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/AbstractByScoreOperation.java
@@ -0,0 +1,63 @@
+package com.github.fppt.jedismock.operations;
+
+import com.github.fppt.jedismock.exception.WrongValueTypeException;
+import com.github.fppt.jedismock.server.Slice;
+import com.github.fppt.jedismock.storage.RedisBase;
+
+import java.util.List;
+import java.util.function.Predicate;
+
+import static com.github.fppt.jedismock.Utils.convertToDouble;
+
+public abstract class AbstractByScoreOperation extends AbstractRedisOperation {
+    protected static final String EXCLUSIVE_PREFIX = "(";
+    private static final String LOWEST_POSSIBLE_SCORE = "-inf";
+    private static final String HIGHEST_POSSIBLE_SCORE = "+inf";
+
+    public AbstractByScoreOperation(RedisBase base, List<Slice> params) {
+        super(base, params);
+    }
+
+    private double getStartScore(String start) {
+        if (LOWEST_POSSIBLE_SCORE.equalsIgnoreCase(start)) {
+            return Double.MIN_VALUE;
+        } else if (start.startsWith(EXCLUSIVE_PREFIX)) {
+            return convertToDouble(start.substring(1));
+        } else {
+            try {
+                return Double.parseDouble(start);
+            } catch (NumberFormatException e) {
+                throw new WrongValueTypeException("Valid start must be a number or start with '" + EXCLUSIVE_PREFIX + "' or be equal to '"
+                        + LOWEST_POSSIBLE_SCORE + "'");
+            }
+        }
+    }
+
+    private double getEndScore(String end) {
+        if (HIGHEST_POSSIBLE_SCORE.equalsIgnoreCase(end)) {
+            return Double.MAX_VALUE;
+        } else if (end.startsWith(EXCLUSIVE_PREFIX)) {
+            return convertToDouble(end.substring(1));
+        } else {
+            try {
+                return Double.parseDouble(end);
+            } catch (NumberFormatException e) {
+                throw new WrongValueTypeException("Valid end must be a number or start with '" + EXCLUSIVE_PREFIX + "' or be equal to '"
+                        + HIGHEST_POSSIBLE_SCORE + "'");
+            }
+        }
+    }
+
+    protected Predicate<Double> getFilterPredicate(String start, String end) {
+        final double startScore = getStartScore(start);
+        final Predicate<Double> compareToStart = p -> start.startsWith(EXCLUSIVE_PREFIX)
+                ? p.compareTo(startScore) > 0
+                : p.compareTo(startScore) >= 0;
+
+        final double endScore = getEndScore(end);
+        final Predicate<Double> compareToEnd = p -> (end.startsWith(EXCLUSIVE_PREFIX)
+                ? p.compareTo(endScore) < 0
+                : p.compareTo(endScore) <= 0);
+        return compareToStart.and(compareToEnd);
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/OperationFactory.java b/src/main/java/com/github/fppt/jedismock/operations/OperationFactory.java
index 3ca7cc0..579a4b5 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/OperationFactory.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/OperationFactory.java
@@ -89,6 +89,7 @@ public class OperationFactory {
         TRANSACTIONAL_OPERATIONS.put("zrange", RO_zrange::new);
         TRANSACTIONAL_OPERATIONS.put("zrangebylex", RO_zrangebylex::new);
         TRANSACTIONAL_OPERATIONS.put("zrem", RO_zrem::new);
+        TRANSACTIONAL_OPERATIONS.put("zrangebyscore", RO_zrangebyscore::new);
         TRANSACTIONAL_OPERATIONS.put("zremrangebyscore", RO_zremrangebyscore::new);
         TRANSACTIONAL_OPERATIONS.put("rename", RO_rename::new);
         TRANSACTIONAL_OPERATIONS.put("zscore", RO_zscore::new);
@@ -121,6 +122,8 @@ public class OperationFactory {
                 return Optional.of(new RO_exec(state));
             case "client":
                 return Optional.of(new RO_client());
+            case "hello":
+                return Optional.of(new RO_hello());
             default:
                 return Optional.empty();
         }
diff --git a/src/main/java/com/github/fppt/jedismock/operations/RO_hello.java b/src/main/java/com/github/fppt/jedismock/operations/RO_hello.java
new file mode 100644
index 0000000..b375cff
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/RO_hello.java
@@ -0,0 +1,19 @@
+package com.github.fppt.jedismock.operations;
+
+import com.github.fppt.jedismock.server.Response;
+import com.github.fppt.jedismock.server.Slice;
+
+import java.util.Arrays;
+
+public class RO_hello implements RedisOperation {
+
+    @Override
+    public Slice execute() {
+        return Response.array(
+                Arrays.asList(
+                        Response.bulkString(Slice.create("proto")),
+                        Response.bulkString(Response.integer(2))
+                )
+        );
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/RO_zrangebyscore.java b/src/main/java/com/github/fppt/jedismock/operations/RO_zrangebyscore.java
new file mode 100644
index 0000000..eae24dc
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/RO_zrangebyscore.java
@@ -0,0 +1,62 @@
+package com.github.fppt.jedismock.operations;
+
+import com.github.fppt.jedismock.server.Response;
+import com.github.fppt.jedismock.server.Slice;
+import com.github.fppt.jedismock.storage.RedisBase;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static com.github.fppt.jedismock.Utils.convertToLong;
+
+public class RO_zrangebyscore extends AbstractByScoreOperation {
+    public RO_zrangebyscore(RedisBase base, List<Slice> params) {
+        super(base, params);
+    }
+
+    @Override
+    Slice response() {
+        final Slice key = params().get(0);
+        final LinkedHashMap<Slice, Double> map = getDataFromBase(key, new LinkedHashMap<>());
+
+        if (map == null || map.isEmpty()) return
+                Response.array(Collections.emptyList());
+
+        final String start = params().get(1).toString();
+        final String end = params().get(2).toString();
+        Predicate<Double> filterPredicate = getFilterPredicate(start, end);
+
+        Stream<Map.Entry<Slice, Double>> entryStream = map.entrySet().stream()
+                .filter(e -> filterPredicate.test(e.getValue()));
+
+        boolean withScores = false;
+        for (int i = 3; i < params().size(); i++) {
+            String param = params().get(i).toString();
+            if ("withscores".equalsIgnoreCase(param)) {
+                withScores = true;
+            } else if ("limit".equalsIgnoreCase(param)) {
+                long offset = convertToLong(params().get(++i).toString());
+                long count = convertToLong(params().get(++i).toString());
+                entryStream = entryStream.skip(offset).limit(count);
+            }
+        }
+
+        Stream<Slice> result;
+        if (withScores) {
+            result = entryStream
+                    .flatMap(e -> Stream.of(e.getKey(),
+                            Slice.create(e.getValue().toString())));
+        } else {
+            result = entryStream
+                    .map(Map.Entry::getKey);
+        }
+        return Response.array(result
+                .map(Response::bulkString)
+                .collect(Collectors.toList()));
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/RO_zremrangebyscore.java b/src/main/java/com/github/fppt/jedismock/operations/RO_zremrangebyscore.java
index b202874..2fbc899 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/RO_zremrangebyscore.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/RO_zremrangebyscore.java
@@ -1,6 +1,5 @@
 package com.github.fppt.jedismock.operations;
 
-import com.github.fppt.jedismock.exception.WrongValueTypeException;
 import com.github.fppt.jedismock.server.Response;
 import com.github.fppt.jedismock.server.Slice;
 import com.github.fppt.jedismock.storage.RedisBase;
@@ -11,15 +10,10 @@ import java.util.Map;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
-import static com.github.fppt.jedismock.Utils.convertToDouble;
 import static com.github.fppt.jedismock.Utils.serializeObject;
 
 
-public class RO_zremrangebyscore extends AbstractRedisOperation {
-
-    private static final String LOWEST_POSSIBLE_SCORE = "-inf";
-    private static final String HIGHEST_POSSIBLE_SCORE = "+inf";
-    private static final String EXCLUSIVE_PREFIX = "(";
+public class RO_zremrangebyscore extends AbstractByScoreOperation {
 
     RO_zremrangebyscore(RedisBase base, List<Slice> params) {
         super(base, params);
@@ -33,26 +27,15 @@ public class RO_zremrangebyscore extends AbstractRedisOperation {
         if (map == null || map.isEmpty()) return Response.integer(0);
 
         final String start = params().get(1).toString();
-        final double startScore = getStartScore(start);
-
-
-        final Predicate<Double> compareToStart = p -> start.startsWith(EXCLUSIVE_PREFIX)
-                ? p.compareTo(startScore) > 0
-                : p.compareTo(startScore) >= 0;
-
         final String end = params().get(2).toString();
-        final double endScore = getEndScore(end);
-
-        final Predicate<Double> compareToEnd = p -> (end.startsWith(EXCLUSIVE_PREFIX)
-                ? p.compareTo(endScore) < 0
-                : p.compareTo(endScore) <= 0);
+        Predicate<Double> filterPredicate = getFilterPredicate(start, end);
 
         List<Double> values = map.values().stream()
-                .filter(compareToStart.and(compareToEnd))
+                .filter(filterPredicate)
                 .collect(Collectors.toList());
 
         final Map<Slice, Double> result = map.entrySet().stream()
-                .filter(entry -> compareToStart.and(compareToEnd).negate().test(entry.getValue()))
+                .filter(entry -> filterPredicate.negate().test(entry.getValue()))
                 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
                         (u, v) -> {
                             //duplicate key
@@ -68,29 +51,4 @@ public class RO_zremrangebyscore extends AbstractRedisOperation {
         return Response.integer(values.size());
     }
 
-    private double getStartScore(String start) {
-        if (LOWEST_POSSIBLE_SCORE.equalsIgnoreCase(start)) {
-            return Double.MIN_VALUE;
-        } else if (start.startsWith(EXCLUSIVE_PREFIX)) {
-            return convertToDouble(start.substring(1));
-        } else if (Character.isDigit(start.charAt(0))) {
-            return convertToDouble(start);
-        } else {
-            throw new WrongValueTypeException("Valid start must be a number or start with '" + EXCLUSIVE_PREFIX + "' or be equal to '"
-                    + LOWEST_POSSIBLE_SCORE + "'");
-        }
-    }
-
-    private double getEndScore(String end) {
-        if (HIGHEST_POSSIBLE_SCORE.equalsIgnoreCase(end)) {
-            return Double.MAX_VALUE;
-        } else if (end.startsWith(EXCLUSIVE_PREFIX)) {
-            return convertToDouble(end.substring(1));
-        } else if (Character.isDigit(end.charAt(0))) {
-            return convertToDouble(end);
-        } else {
-            throw new WrongValueTypeException("Valid end must be a number or start with '" + EXCLUSIVE_PREFIX + "' or be equal to '"
-                    + HIGHEST_POSSIBLE_SCORE + "'");
-        }
-    }
 }


Test patch:
diff --git a/src/test/java/com/github/fppt/jedismock/TestLettuceConnection.java b/src/test/java/com/github/fppt/jedismock/TestLettuceConnection.java
new file mode 100644
index 0000000..6b118ef
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/TestLettuceConnection.java
@@ -0,0 +1,36 @@
+package com.github.fppt.jedismock;
+
+import io.lettuce.core.ClientOptions;
+import io.lettuce.core.RedisClient;
+import io.lettuce.core.api.StatefulRedisConnection;
+import io.lettuce.core.api.sync.RedisCommands;
+import io.lettuce.core.protocol.ProtocolVersion;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+
+public class TestLettuceConnection {
+
+    @Test
+    void lettuceClientCanConnectAndWork() throws IOException {
+        RedisServer server = RedisServer.newRedisServer();
+        server.start();
+        try {
+            RedisClient redisClient = RedisClient
+                    .create(String.format("redis://%s:%s",
+                            server.getHost(), server.getBindPort()));
+            StatefulRedisConnection<String, String> connection = redisClient.connect();
+            RedisCommands<String, String> syncCommands = connection.sync();
+            syncCommands.set("key", "Hello, Redis!");
+            String val = syncCommands.get("key");
+            connection.close();
+            redisClient.shutdown();
+
+            assertEquals("Hello, Redis!", val);
+        } finally {
+            server.stop();
+        }
+    }
+}
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/TestZRangeByScore.java b/src/test/java/com/github/fppt/jedismock/comparisontests/TestZRangeByScore.java
new file mode 100644
index 0000000..3831ec7
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/TestZRangeByScore.java
@@ -0,0 +1,183 @@
+package com.github.fppt.jedismock.comparisontests;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.Tuple;
+import redis.clients.jedis.exceptions.JedisDataException;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import static java.util.Arrays.asList;
+import static org.junit.jupiter.api.Assertions.*;
+
+@ExtendWith(ComparisonBase.class)
+public class TestZRangeByScore {
+
+    private static final String ZSET_KEY = "myzset";
+
+    @BeforeEach
+    public void clearKey(Jedis jedis) {
+        jedis.del(ZSET_KEY);
+    }
+
+    @TestTemplate
+    public void whenUsingZrangeByScore_EnsureItReturnsEmptySetForNonDefinedKey(Jedis jedis) {
+        assertEquals(Collections.emptySet(), jedis.zrangeByScore(ZSET_KEY, "-inf", "+inf"));
+        assertEquals(Collections.emptySet(), jedis.zrangeByScoreWithScores(ZSET_KEY, "-inf", "+inf"));
+    }
+
+    @TestTemplate
+    public void whenUsingZrangeByScore_EnsureItReturnsEverythingWithPlusMinusInfinity(Jedis jedis) {
+        jedis.zadd(ZSET_KEY, 1, "one");
+        jedis.zadd(ZSET_KEY, 1, "two");
+        jedis.zadd(ZSET_KEY, 1, "three");
+        assertEquals(new HashSet(asList("one", "two", "three")),
+                jedis.zrangeByScore(ZSET_KEY, "-inf", "+inf"));
+        assertEquals(new HashSet(asList(new Tuple("one", 1.),
+                new Tuple("two", 1.), new Tuple("three", 1.))),
+                jedis.zrangeByScoreWithScores(ZSET_KEY, "-inf", "+inf"));
+    }
+
+
+    @TestTemplate
+    public void whenUsingZrangeByScore_EnsureItReturnsSetWhenLowestAndHighestScoresSpecified(Jedis jedis) {
+        // given
+        jedis.zadd(ZSET_KEY, 1, "one");
+        jedis.zadd(ZSET_KEY, 2, "two");
+        jedis.zadd(ZSET_KEY, 3, "three");
+        assertEquals(3, jedis.zrange(ZSET_KEY, 0, -1).size());
+
+        // when
+        final Set<String> zrangeByScoreResult = jedis.zrangeByScore(ZSET_KEY, "-inf", "+inf");
+
+        // then
+        assertEquals(new HashSet(asList("one", "two", "three")), zrangeByScoreResult);
+        assertEquals(new HashSet(asList(new Tuple("one", 1.),
+                new Tuple("two", 2.), new Tuple("three", 3.))),
+                jedis.zrangeByScoreWithScores(ZSET_KEY, "-inf", "+inf"));
+
+    }
+
+    @TestTemplate
+    public void whenUsingzrangeByScore_EnsureItReturnsValueWhenIntervalSpecified(Jedis jedis) {
+        // given
+        jedis.zadd(ZSET_KEY, 1, "one");
+        jedis.zadd(ZSET_KEY, 2, "two");
+        jedis.zadd(ZSET_KEY, 3, "three");
+        assertEquals(3, jedis.zrange(ZSET_KEY, 0, -1).size());
+
+        // when
+        final Set<String> zrangeByScoreResult = jedis.zrangeByScore(ZSET_KEY, "-inf", "2");
+
+        // then
+        assertEquals(new HashSet(asList("one", "two")), zrangeByScoreResult);
+
+    }
+
+    @TestTemplate
+    public void whenUsingzrangeByScore_EnsureItDoesNotReturnValueWhenExclusiveIntervalSpecified(Jedis jedis) {
+        // given
+        jedis.zadd(ZSET_KEY, 1, "one");
+        jedis.zadd(ZSET_KEY, 2, "two");
+        jedis.zadd(ZSET_KEY, 3, "three");
+        assertEquals(3, jedis.zrange(ZSET_KEY, 0, -1).size());
+
+        // then
+        assertEquals(Collections.singleton("one"), jedis.zrangeByScore(ZSET_KEY, "-inf", "(2"));
+        assertEquals(Collections.singleton(new Tuple("one", 1.)),
+                jedis.zrangeByScoreWithScores(ZSET_KEY, "-inf", "(2"));
+    }
+
+    @TestTemplate
+    public void whenUsingzrangeByScore_EnsureItReturnsValuesAccordingToSpecifiedInterval(Jedis jedis) {
+        // given
+        jedis.zadd(ZSET_KEY, 1, "one");
+        jedis.zadd(ZSET_KEY, 2, "two");
+        jedis.zadd(ZSET_KEY, 3, "three");
+        jedis.zadd(ZSET_KEY, 4, "four");
+        jedis.zadd(ZSET_KEY, 5, "five");
+        jedis.zadd(ZSET_KEY, 7, "seven");
+        jedis.zadd(ZSET_KEY, 6, "six");
+        jedis.zadd(ZSET_KEY, 8, "eight");
+        jedis.zadd(ZSET_KEY, 9, "nine");
+        jedis.zadd(ZSET_KEY, 10, "ten");
+        assertEquals(10, jedis.zrange(ZSET_KEY, 0, -1).size());
+
+        //then
+        assertEquals(new HashSet(asList("five", "six", "seven", "eight")),
+                jedis.zrangeByScore(ZSET_KEY, 5, 8));
+        assertEquals(new HashSet(asList(
+                new Tuple("five", 5.),
+                new Tuple("six", 6.),
+                new Tuple("seven", 7.),
+                new Tuple("eight", 8.))),
+                jedis.zrangeByScoreWithScores(ZSET_KEY, 5, 8));
+    }
+
+
+    @TestTemplate
+    public void whenUsingzrangeByScore_EnsureItReturnsValuesAccordingToSpecifiedIntervalWithNegative(Jedis jedis) {
+        // given
+        jedis.zadd(ZSET_KEY, -2, "minustwo");
+        jedis.zadd(ZSET_KEY, -1, "minusone");
+        jedis.zadd(ZSET_KEY, 0, "zero");
+        jedis.zadd(ZSET_KEY, 1, "one");
+        jedis.zadd(ZSET_KEY, 2, "two");
+
+        //then
+        assertEquals(new HashSet(asList("minusone", "zero", "one")),
+                jedis.zrangeByScore(ZSET_KEY, -1, 1));
+        assertEquals(new HashSet(asList(
+                new Tuple("minusone", -1.),
+                new Tuple("zero", 0.),
+                new Tuple("one", 1.))),
+                jedis.zrangeByScoreWithScores(ZSET_KEY, -1, 1));
+    }
+
+    @TestTemplate
+    public void whenUsingzrangeByScore_EnsureItReturnsValuesWithLimitAndOffset(Jedis jedis) {
+        // given
+        jedis.zadd(ZSET_KEY, 1, "one");
+        jedis.zadd(ZSET_KEY, 2, "two");
+        jedis.zadd(ZSET_KEY, 4, "four");
+        jedis.zadd(ZSET_KEY, 3, "three");
+        jedis.zadd(ZSET_KEY, 5, "five");
+        jedis.zadd(ZSET_KEY, 7, "seven");
+        jedis.zadd(ZSET_KEY, 6, "six");
+        jedis.zadd(ZSET_KEY, 8, "eight");
+        jedis.zadd(ZSET_KEY, 9, "nine");
+        jedis.zadd(ZSET_KEY, 10, "ten");
+        assertEquals(10, jedis.zrange(ZSET_KEY, 0, -1).size());
+
+        //then
+      //  assertEquals(new HashSet(asList("three", "four")),
+      //          jedis.zrangeByScore(ZSET_KEY, 2, 5, 1, 2));
+        assertEquals(new HashSet(asList(
+                new Tuple("three", 3.),
+                new Tuple("four", 4.))),
+                jedis.zrangeByScoreWithScores(ZSET_KEY, 2, 5, 1, 2));
+    }
+
+    @TestTemplate
+    public void whenUsingzrangeByScore_EnsureItThrowsExceptionsWhenStartAndEndHaveWrongFormat(Jedis jedis) {
+        // given
+        jedis.zadd(ZSET_KEY, 1, "one");
+        jedis.zadd(ZSET_KEY, 2, "two");
+        jedis.zadd(ZSET_KEY, 3, "three");
+
+        // then
+        assertThrows(JedisDataException.class,
+                () -> jedis.zrangeByScore(ZSET_KEY, "(dd", "(sd"));
+        assertThrows(JedisDataException.class,
+                () -> jedis.zrangeByScoreWithScores(ZSET_KEY, "(dd", "(sd"));
+        assertThrows(JedisDataException.class,
+                () -> jedis.zrangeByScore(ZSET_KEY, "1.e", "2.d"));
+        assertThrows(RuntimeException.class,
+                () -> jedis.zrangeByScore(ZSET_KEY, "FOO", "BAR"));
+    }
+}