Repository: fppt/jedis-mock
Pull request #31 (2019-03-02 00:02:18)

Multiple fixes
It became impossible to do all this stuff in separate PRs, so I combined everything into one.

Here's the full list of  changes:
* Implemented `scard` and `srem` per #24 and #21.
* Fixed bug in `sadd`. This command should return the number of added entries, not the size of resulting set.
* Fixed bug in `exists`. Always returned `false` for HMAPs.
* Fixed bug in `del`. It didn't delete HMAPs.
* Implemented `time`. Since Java 8 has no wallclock with microsecond resolution, the microseconds field of `time` result will always be rounded up to a millisecond, but I suppose it's OK for a mock.
* `pom.xml`: set `test` scope for embedded-redis dependency. 


Patch:
diff --git a/pom.xml b/pom.xml
index 05e5d73..f87dadd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -81,6 +81,7 @@
             <groupId>com.github.kstyrc</groupId>
             <artifactId>embedded-redis</artifactId>
             <version>${redis-embedded.version}</version>
+            <scope>test</scope>
         </dependency>
         <!-- Used for finding common bugs in java projects -->
         <dependency>
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 c931c26..2d068ae 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/OperationFactory.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/OperationFactory.java
@@ -57,6 +57,8 @@ public class OperationFactory {
         TRANSACTIONAL_OPERATIONS.put("keys", RO_keys::new);
         TRANSACTIONAL_OPERATIONS.put("sadd", RO_sadd::new);
         TRANSACTIONAL_OPERATIONS.put("spop", RO_spop::new);
+        TRANSACTIONAL_OPERATIONS.put("srem", RO_srem::new);
+        TRANSACTIONAL_OPERATIONS.put("scard", RO_scard::new);
         TRANSACTIONAL_OPERATIONS.put("hget", RO_hget::new);
         TRANSACTIONAL_OPERATIONS.put("hset", RO_hset::new);
         TRANSACTIONAL_OPERATIONS.put("hdel", RO_hdel::new);
@@ -66,6 +68,7 @@ public class OperationFactory {
         TRANSACTIONAL_OPERATIONS.put("hmset", RO_hmset::new);
         TRANSACTIONAL_OPERATIONS.put("smembers", RO_smembers::new);
         TRANSACTIONAL_OPERATIONS.put("hsetnx", RO_hsetnx::new);
+        TRANSACTIONAL_OPERATIONS.put("time", RO_time::new);
     }
 
 
diff --git a/src/main/java/com/github/fppt/jedismock/operations/RO_del.java b/src/main/java/com/github/fppt/jedismock/operations/RO_del.java
index 12b41f0..f8bc156 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/RO_del.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/RO_del.java
@@ -11,12 +11,11 @@ class RO_del extends AbstractRedisOperation {
         super(base, params);
     }
 
-    Slice response(){
+    Slice response() {
         int count = 0;
         for (Slice key : params()) {
-            Slice value = base().getValue(key);
-            base().deleteValue(key);
-            if (value != null) {
+            if (base().exists(key)) {
+                base().deleteValue(key);
                 count++;
             }
         }
diff --git a/src/main/java/com/github/fppt/jedismock/operations/RO_exists.java b/src/main/java/com/github/fppt/jedismock/operations/RO_exists.java
index ebc95e7..b3b75e1 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/RO_exists.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/RO_exists.java
@@ -12,7 +12,7 @@ class RO_exists extends AbstractRedisOperation {
     }
 
     Slice response() {
-        if (base().getValue(params().get(0)) != null) {
+        if (base().exists(params().get(0))) {
             return Response.integer(1);
         }
         return Response.integer(0);
diff --git a/src/main/java/com/github/fppt/jedismock/operations/RO_sadd.java b/src/main/java/com/github/fppt/jedismock/operations/RO_sadd.java
index 8aba14f..87c9516 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/RO_sadd.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/RO_sadd.java
@@ -1,5 +1,6 @@
 package com.github.fppt.jedismock.operations;
 
+import com.github.fppt.jedismock.server.Response;
 import com.github.fppt.jedismock.storage.RedisBase;
 import com.github.fppt.jedismock.server.Slice;
 
@@ -7,20 +8,29 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
-class RO_sadd extends RO_add<Set<Slice>> {
+import static com.github.fppt.jedismock.Utils.serializeObject;
+
+class RO_sadd extends AbstractRedisOperation {
     RO_sadd(RedisBase base, List<Slice> params) {
         super(base, params);
     }
 
     @Override
-    void addSliceToCollection(Set<Slice> set, Slice slice) {
+    Slice response() {
+        Slice key = params().get(0);
+        Set<Slice> set = getDataFromBase(key, new HashSet<>());
+
+        int count = 0;
         for (int i = 1; i < params().size(); i++) {
-            set.add(params().get(i));
+            if (set.add(params().get(i))){
+                count++;
+            }
         }
-    }
-
-    @Override
-    Set<Slice> getDefaultResponse() {
-        return new HashSet<>();
+        try {
+            base().putValue(key, serializeObject(set));
+        } catch (Exception e) {
+            throw new RuntimeException(e.getMessage());
+        }
+        return Response.integer(count);
     }
 }
diff --git a/src/main/java/com/github/fppt/jedismock/operations/RO_scard.java b/src/main/java/com/github/fppt/jedismock/operations/RO_scard.java
new file mode 100644
index 0000000..60e25e1
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/RO_scard.java
@@ -0,0 +1,22 @@
+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.List;
+import java.util.Set;
+
+class RO_scard extends AbstractRedisOperation {
+
+    RO_scard(RedisBase base, List<Slice> params) {
+        super(base, params);
+    }
+
+    Slice response() {
+        Slice key = params().get(0);
+        Set<Slice> set = getDataFromBase(key, null);
+        if(set == null || set.isEmpty()) return Response.integer(0);
+        return Response.integer(set.size());
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/RO_srem.java b/src/main/java/com/github/fppt/jedismock/operations/RO_srem.java
new file mode 100644
index 0000000..306b3a5
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/RO_srem.java
@@ -0,0 +1,36 @@
+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.List;
+import java.util.Set;
+
+import static com.github.fppt.jedismock.Utils.serializeObject;
+
+class RO_srem extends AbstractRedisOperation {
+
+
+    RO_srem(RedisBase base, List<Slice> params) {
+        super(base, params);
+    }
+
+    Slice response() {
+        Slice key = params().get(0);
+        Set<Slice> set = getDataFromBase(key, null);
+        if(set == null || set.isEmpty()) return Response.integer(0);
+        int count = 0;
+        for (int i = 1; i < params().size(); i++) {
+            if (set.remove(params().get(i))) {
+                count++;
+            }
+        }
+        try {
+            base().putValue(key, serializeObject(set));
+        } catch (Exception e) {
+            throw new RuntimeException(e.getMessage());
+        }
+        return Response.integer(count);
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/RO_time.java b/src/main/java/com/github/fppt/jedismock/operations/RO_time.java
new file mode 100644
index 0000000..53675f4
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/RO_time.java
@@ -0,0 +1,26 @@
+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.Arrays;
+import java.util.List;
+
+public class RO_time extends AbstractRedisOperation {
+    RO_time(RedisBase base, List<Slice> params) {
+        super(base, params);
+    }
+
+    @Override
+    Slice response() {
+        //Java8 has wallclock with microseconds precision,
+        //so this mock returns results truncated up to a millisecond
+        long time = System.currentTimeMillis();
+        long seconds = time / 1000L;
+        long microseconds = (time % 1000L) * 1000L;
+        return Response.array(Arrays.asList(
+                Response.bulkString(Slice.create(Long.toString(seconds))),
+                Response.bulkString(Slice.create(Long.toString(microseconds)))));
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/storage/ExpiringKeyValueStorage.java b/src/main/java/com/github/fppt/jedismock/storage/ExpiringKeyValueStorage.java
index 12d2fc2..c8be499 100644
--- a/src/main/java/com/github/fppt/jedismock/storage/ExpiringKeyValueStorage.java
+++ b/src/main/java/com/github/fppt/jedismock/storage/ExpiringKeyValueStorage.java
@@ -21,8 +21,9 @@ public abstract class ExpiringKeyValueStorage {
         return new AutoValue_ExpiringKeyValueStorage(HashBasedTable.create(), HashBasedTable.create());
     }
 
-    public void delete(Slice key){
-        delete(key, Slice.reserved());
+    public void delete(Slice key) {
+        for (Slice key2 : values().row(key).keySet())
+            delete(key, key2);
     }
 
     public void delete(Slice key1, Slice key2){
@@ -121,4 +122,17 @@ public abstract class ExpiringKeyValueStorage {
         }
         return 0L;
     }
+
+    public boolean exists(Slice slice) {
+        if (values().containsRow(slice)) {
+            Long deadline = ttls().get(slice, Slice.reserved());
+            if (deadline != null && deadline != -1 && deadline <= System.currentTimeMillis()) {
+                delete(slice, Slice.reserved());
+                return false;
+            } else {
+                return true;
+            }
+        }
+        return false;
+    }
 }
diff --git a/src/main/java/com/github/fppt/jedismock/storage/RedisBase.java b/src/main/java/com/github/fppt/jedismock/storage/RedisBase.java
index ef094ca..833cd68 100644
--- a/src/main/java/com/github/fppt/jedismock/storage/RedisBase.java
+++ b/src/main/java/com/github/fppt/jedismock/storage/RedisBase.java
@@ -108,4 +108,8 @@ public class RedisBase {
 
         return subscriptions;
     }
+
+    public boolean exists(Slice slice) {
+        return keyValueStorage.exists(slice);
+    }
 }


Test patch:
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/SimpleOperationsTest.java b/src/test/java/com/github/fppt/jedismock/comparisontests/SimpleOperationsTest.java
index b081d92..8c0a5f5 100644
--- a/src/test/java/com/github/fppt/jedismock/comparisontests/SimpleOperationsTest.java
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/SimpleOperationsTest.java
@@ -206,13 +206,58 @@ public class SimpleOperationsTest extends ComparisonBase {
     }
 
     @Theory
-    public void whenPoppingFromASet_EnsureTheSetIsUpdated(Jedis jedis){
+    public void whenDuplicateValuesAddedToSet_ReturnsAddedValuesCountOnly(Jedis jedis) {
+        String key = "my-set-key-sadd";
+        assertEquals(3, jedis.sadd(key, "A", "B", "C", "B").intValue());
+        assertEquals(1, jedis.sadd(key, "A", "C", "E", "B").intValue());
+    }
+
+    @Theory
+    public void whenAddingToASet_ensureCountIsUpdated(Jedis jedis){
+        String key = "my-counted-set-key";
+        Set<String> mySet = new HashSet<>(Arrays.asList("d", "e", "f"));
+
+        //Add everything from the set
+        mySet.forEach(value -> jedis.sadd(key, value));
+
+        //Get it all back
+        assertEquals(mySet.size(), jedis.scard(key).intValue());
+    }
+
+    @Theory
+    public void whenCalledForNonExistentSet_ensureScardReturnsZero(Jedis jedis){
+        String key = "non-existent";
+        assertEquals(0, jedis.scard(key).intValue());
+    }
+
+    @Theory
+    public void whenRemovingFromASet_EnsureTheSetIsUpdated(Jedis jedis){
         String key = "my-set-key";
         Set<String> mySet = new HashSet<>(Arrays.asList("a", "b", "c", "d"));
 
         //Add everything from the set
         mySet.forEach(value -> jedis.sadd(key, value));
 
+        // Remove an element
+        mySet.remove("c");
+        mySet.remove("d");
+        mySet.remove("f");
+        int removed = jedis.srem(key, "c", "d", "f").intValue();
+
+        //Get it all back
+        assertEquals(mySet, jedis.smembers(key));
+        assertEquals(2, removed);
+    }
+
+    @Theory
+    public void whenPoppingFromASet_EnsureTheSetIsUpdated(Jedis jedis){
+
+        String key = "my-set-key-spop";
+        Set<String> mySet = new HashSet<>(Arrays.asList("a", "b", "c", "d"));
+
+        //Add everything from the set
+        mySet.forEach(value -> jedis.sadd(key, value));
+
         String poppedValue;
         do {
             poppedValue = jedis.spop(key);
@@ -344,4 +389,39 @@ public class SimpleOperationsTest extends ComparisonBase {
     public void whenGettingInfo_EnsureSomeDateIsReturned(Jedis jedis){
         assertNotNull(jedis.info());
     }
+
+    @Theory
+    public void whenCreatingKeys_existsValuesUpdated(Jedis jedis){
+        jedis.set("foo", "bar");
+        assertTrue(jedis.exists("foo"));
+
+        assertFalse(jedis.exists("non-existent"));
+
+        jedis.hset("bar", "baz", "value");
+        assertTrue(jedis.exists("bar"));
+    }
+
+    @Theory
+    public void deletionRemovesKeys(Jedis jedis){
+        String key1 = "hey_toremove";
+        String key2 = "hmap_toremove";
+        jedis.set(key1, "value");
+        jedis.hset(key2, "field", "value");
+        assertTrue(jedis.exists(key1));
+        assertTrue(jedis.exists(key2));
+        int count = jedis.del(key1, key2).intValue();
+        assertEquals(2, count);
+        assertFalse(jedis.exists(key1));
+        assertFalse(jedis.exists(key2));
+    }
+
+    @Theory
+    public void timeReturnsCurrentTime(Jedis jedis) {
+        long currentTime = System.currentTimeMillis() / 1000;
+        List<String> time = jedis.time();
+        //We believe that results difference will be within one second
+        assertTrue(Math.abs(currentTime - Long.parseLong(time.get(0))) < 2);
+        //Microseconds are correct integer value
+        Long.parseLong(time.get(1));
+    }
 }