Repository: fppt/jedis-mock
Pull request #327 (2023-05-21 22:23:14)

Cumulative update for version 1.0.8
Spring 2023 update


Patch:
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index cd43e45..308f108 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -28,6 +28,14 @@ jobs:
       - name: Build with Maven
         run: mvn -B -V verify -Dmaven.javadoc.skip
 
+      - name: Publish test report
+        uses: dorny/test-reporter@v1
+        if: (success() || failure()) && github.actor != 'dependabot[bot]'
+        with:
+          name: Test report
+          path: '**/surefire-reports/**/*.xml'
+          reporter: java-junit
+
       - name: Update list of supported operations
         if: success() && github.ref == 'refs/heads/master'
         uses: stefanzweifel/git-auto-commit-action@v4
diff --git a/README.md b/README.md
index 4688e33..d6ec799 100644
--- a/README.md
+++ b/README.md
@@ -8,24 +8,30 @@ Jedis-Mock is a simple in-memory mock of Redis for Java testing, which can also
 Despite its name, it works on network protocol level and can be used with any Redis client 
 (be it `Jedis`, `Lettuce` or others).
 
-When used as a mock, it allows you to test any behaviour dependent on Redis without having to deploy an instance of Redis.
+When used as a mock, it allows you to test behaviour dependent on Redis without having to deploy an instance of Redis.
 
-[List of currently supported operations](supported_operations.md)
+[List of currently supported operations](supported_operations.md).
 
-## Why, if we already have TestContainers?
-TestContainers is a great solution for integration tests with real services, including Redis.
-
-However, sometimes we want to use mock or proxy for some of our tests for the following reasons:
+## Why, if we have TestContainers?
+TestContainers is a great solution for integration tests with real services, including Redis. However, sometimes we want to use mock or proxy for some of our tests for the following reasons:
 
 * TestContainers require Docker. Jedis-Mock is just a Maven dependency which, when used as 'pure' mock, can be run on any machine, right now.
-* TestContainers tests can be slow and extremely resource-consuming. Jedis-Mock tests are lightning fast, which
+* TestContainers tests can be slow and resource-consuming. Jedis-Mock tests are lightning fast, which
 encourages developers to write more tests and run them more often.
 * Redis running in TestContainers is a "black box". We cannot verify what was actually called. 
   We cannot interfere with the reply. All this we can do with testing mock/proxy.
 * If you wish, you can use Jedis-Mock *together* with TestContainers, delegating command execution 
   to a real Redis instance, intercepting some of the calls when needed.
 
+## How can I ensure that this mock functions exactly like real Redis?
+
+First and foremost, it's important to acknowledge that no test mock can behave with absolute precision identical to a real system. Attempting to create a bug-to-bug compatible reimplementation would be impractical. The primary objective of a test mock is to expose errors in the code being tested. Therefore, it is acceptable for a mock to fail more frequently than a real system.
+
+In the case of Jedis-Mock, we make diligent efforts to preserve the semantics of Redis operations. We employ two practices to achieve this:
 
+1. Extensive use of comparison testing: All tests for Jedis-Mock are executed twice—once against the mock and once against a real Redis instance running in TestContainers. This approach ensures that our tests for Jedis-Mock include accurate assertions.
+
+2. Execution of native Redis tests: We continuously expand the suite of native Redis tests that are successfully executed against Jedis-Mock. These tests are the ones employed for regression testing of Redis itself. You can explore the specific tests being executed [here](.github/workflows/native-tests.yml).
 
 ## Quickstart 
 
@@ -90,7 +96,7 @@ try (Jedis jedis = new Jedis(server.getHost(), server.getBindPort())) {
 server.stop();
 ```
 
-WARNING: if you are going to mutate the shared state, synchronize on `state.lock()` first!
+:warning: if you are going to mutate the shared state, synchronize on `state.lock()` first!
 (See how it's done in [`MockExecutor#proceed`](src/main/java/com/github/fppt/jedismock/operations/server/MockExecutor.java#L23)). 
 
 ## Fault tolerance testing
@@ -112,9 +118,31 @@ try (Jedis jedis = new Jedis(server.getHost(),
 }
 ```
 
+## Lua scripting support
+
+JedisMock supports Lua scripting (`EVAL`, `EVALSHA`, `SCRIPT LOAD/EXISTS/FLUSH` commands) via [luaj](https://github.com/luaj/luaj).  
+
+```java
+String script =
+                "local a, b = 0, 1\n" +
+                "for i = 2, ARGV[1] do\n" +
+                "  local temp = a + b\n" +
+                "  a = b\n" +
+                "  b = temp\n" +
+                "  redis.call('RPUSH',KEYS[1], temp)\n" +
+                "end\n" ;
+jedis.eval(script, 1, "mylist", "10");
+//Yields first 10 Fibonacci numbers
+jedis.lrange("mylist", 0, -1));        
+```
+
+:warning: Lua language capabilities are restricted to what is provided by current LuaJ version. Methods provided by `redis` global object are currently restricted to what was available in Redis version 2.6.0 (see [redis.lua](src/main/resources/redis.lua)). 
+
+Feel free to report an issue if you have any problems with Lua scripting in Jedis-Mock.
+
 ## Supported and Missing Operations
 
-All currently supported and missing operations are listed [here](supported_operations.md)
+All currently supported and missing operations are listed [here](supported_operations.md).
 
 If you get the following error:
 
diff --git a/pom.xml b/pom.xml
index 81b524f..2a19ab1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -43,10 +43,10 @@
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
         <junit.version>5.9.1</junit.version>
-        <jedis.version>4.3.0</jedis.version>
-        <testcontainers.version>1.17.6</testcontainers.version>
+        <jedis.version>4.3.1</jedis.version>
+        <testcontainers.version>1.18.1</testcontainers.version>
         <spotbugs.version>4.7.2.1</spotbugs.version>
-        <slf4j.version>2.0.6</slf4j.version>
+        <slf4j.version>2.0.7</slf4j.version>
     </properties>
 
     <distributionManagement>
@@ -61,23 +61,45 @@
     </distributionManagement>
 
     <dependencies>
+        <!-- transitive dependencies -->
         <dependency>
-            <groupId>org.junit.jupiter</groupId>
-            <artifactId>junit-jupiter</artifactId>
-            <version>${junit.version}</version>
-            <scope>test</scope>
+            <groupId>org.reflections</groupId>
+            <artifactId>reflections</artifactId>
+            <version>0.10.2</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>${slf4j.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.luaj</groupId>
+            <artifactId>luaj-jse</artifactId>
+            <version>3.0.1</version>
         </dependency>
-        <!-- Used for creating Jedis Client Connections -->
         <dependency>
             <groupId>redis.clients</groupId>
             <artifactId>jedis</artifactId>
             <version>${jedis.version}</version>
+        </dependency>
+
+        <!-- test dependencies -->
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <version>${junit.version}</version>
             <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>io.lettuce</groupId>
             <artifactId>lettuce-core</artifactId>
-            <version>6.2.1.RELEASE</version>
+            <version>6.2.3.RELEASE</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <version>2.0.7</version>
             <scope>test</scope>
         </dependency>
         <!-- Used for creating an testcontainer of Redis for testing and comparing behaviour-->
@@ -99,23 +121,12 @@
             <version>4.11.0</version>
             <scope>test</scope>
         </dependency>
-		<dependency>
+        <dependency>
             <groupId>nl.jqno.equalsverifier</groupId>
             <artifactId>equalsverifier</artifactId>
-            <version>3.14</version>
+            <version>3.14.1</version>
             <scope>test</scope>
         </dependency>
-        <!-- SLF4J is the logging API all modules should use -->
-        <dependency>
-            <groupId>org.slf4j</groupId>
-            <artifactId>slf4j-api</artifactId>
-            <version>${slf4j.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.reflections</groupId>
-            <artifactId>reflections</artifactId>
-            <version>0.10.2</version>
-        </dependency>
     </dependencies>
 
     <build>
@@ -146,7 +157,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-surefire-plugin</artifactId>
-                <version>2.22.2</version>
+                <version>3.1.0</version>
                 <configuration>
                     <useFile>true</useFile>
                     <redirectTestOutputToFile>true</redirectTestOutputToFile>
@@ -155,7 +166,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-release-plugin</artifactId>
-                <version>2.5.3</version>
+                <version>3.0.0</version>
                 <configuration>
                     <autoVersionSubmodules>true</autoVersionSubmodules>
                     <useReleaseProfile>false</useReleaseProfile>
@@ -198,7 +209,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-pmd-plugin</artifactId>
-                <version>3.20.0</version>
+                <version>3.21.0</version>
                 <configuration>
                     <verbose>true</verbose>
                     <rulesets>
@@ -217,7 +228,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
-                <version>3.2.1</version>
+                <version>3.2.2</version>
                 <executions>
                     <execution>
                         <id>validate</id>
@@ -262,7 +273,7 @@
                     <plugin>
                         <groupId>org.apache.maven.plugins</groupId>
                         <artifactId>maven-gpg-plugin</artifactId>
-                        <version>3.0.1</version>
+                        <version>3.1.0</version>
                         <executions>
                             <execution>
                                 <id>sign-artifacts</id>
diff --git a/src/main/java/com/github/fppt/jedismock/datastructures/RMHash.java b/src/main/java/com/github/fppt/jedismock/datastructures/RMHash.java
index bb80ae6..2ed0a86 100644
--- a/src/main/java/com/github/fppt/jedismock/datastructures/RMHash.java
+++ b/src/main/java/com/github/fppt/jedismock/datastructures/RMHash.java
@@ -7,34 +7,12 @@ import java.util.Map;
 public class RMHash implements RMDataStructure {
     private final LinkedHashMap<Slice, Slice> storedData = new LinkedHashMap<>();
 
-    private String encoding = "ziplist";
-
     public Map<Slice, Slice> getStoredData() {
         return storedData;
     }
 
-    public void upgradeEncoding() {
-        this.encoding = "hashtable";
-    }
-
-    public void downgradeEncoding() {
-        this.encoding = "ziplist";
-    }
-
-    public String getEncoding() {
-        return encoding;
-    }
-
-    public String getMeta() {
-        // here could be added some more metainformation later
-        return " encoding:" + encoding + " ";
-    }
-
     public void put(Slice key, Slice data) {
         storedData.put(key, data);
-        if (storedData.size() > 31) {
-            this.upgradeEncoding();
-        }
     }
 
     @Override
diff --git a/src/main/java/com/github/fppt/jedismock/datastructures/RMList.java b/src/main/java/com/github/fppt/jedismock/datastructures/RMList.java
index 6f94cb3..6bf5155 100644
--- a/src/main/java/com/github/fppt/jedismock/datastructures/RMList.java
+++ b/src/main/java/com/github/fppt/jedismock/datastructures/RMList.java
@@ -6,7 +6,15 @@ import java.util.ArrayList;
 import java.util.List;
 
 public class RMList implements RMDataStructure {
-    private final List<Slice> storedData = new ArrayList<>();
+    private final List<Slice> storedData;
+
+    public RMList() {
+        this.storedData = new ArrayList<>();
+    }
+
+    public RMList(List<Slice> storedData) {
+        this.storedData = storedData;
+    }
 
     public List<Slice> getStoredData() {
         return storedData;
diff --git a/src/main/java/com/github/fppt/jedismock/exception/EOFException.java b/src/main/java/com/github/fppt/jedismock/exception/EOFException.java
index 1e4f383..be6baea 100644
--- a/src/main/java/com/github/fppt/jedismock/exception/EOFException.java
+++ b/src/main/java/com/github/fppt/jedismock/exception/EOFException.java
@@ -3,7 +3,7 @@ package com.github.fppt.jedismock.exception;
 /**
  * Created by Xiaolu on 2015/4/21.
  */
-public class EOFException extends Exception {
+public class EOFException extends RuntimeException {
 
     public EOFException() {
         super();
diff --git a/src/main/java/com/github/fppt/jedismock/operations/hashes/HSet.java b/src/main/java/com/github/fppt/jedismock/operations/hashes/HSet.java
index fda0081..b0d7d13 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/hashes/HSet.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/hashes/HSet.java
@@ -3,7 +3,6 @@ package com.github.fppt.jedismock.operations.hashes;
 import com.github.fppt.jedismock.operations.AbstractRedisOperation;
 import com.github.fppt.jedismock.operations.RedisCommand;
 import com.github.fppt.jedismock.server.Response;
-import com.github.fppt.jedismock.datastructures.RMHash;
 import com.github.fppt.jedismock.datastructures.Slice;
 import com.github.fppt.jedismock.storage.RedisBase;
 
@@ -34,13 +33,8 @@ class HSet extends AbstractRedisOperation {
         for(int i = 1; i < params().size(); i = i + 2){
             Slice field = params().get(i);
             Slice value = params().get(i+1);
-            if (value.length() > 1000) {
-                RMHash cls = base().getHash(hash);
-                cls.upgradeEncoding();
-            }
             Slice oldValue = hsetValue(hash, field, value);
-
-            if(oldValue == null) {
+            if (oldValue == null) {
                 count++;
             }
         }
diff --git a/src/main/java/com/github/fppt/jedismock/operations/keys/Rename.java b/src/main/java/com/github/fppt/jedismock/operations/keys/Rename.java
index 5ba8546..d181a06 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/keys/Rename.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/keys/Rename.java
@@ -1,19 +1,22 @@
 package com.github.fppt.jedismock.operations.keys;
 
 import com.github.fppt.jedismock.datastructures.RMDataStructure;
+import com.github.fppt.jedismock.datastructures.Slice;
 import com.github.fppt.jedismock.operations.AbstractRedisOperation;
 import com.github.fppt.jedismock.operations.RedisCommand;
 import com.github.fppt.jedismock.server.Response;
-import com.github.fppt.jedismock.datastructures.Slice;
-import com.github.fppt.jedismock.storage.RedisBase;
+import com.github.fppt.jedismock.storage.OperationExecutorState;
 
 import java.util.List;
 
 @RedisCommand("rename")
 class Rename extends AbstractRedisOperation {
 
-    Rename(RedisBase base, List<Slice> params) {
-        super(base, params);
+    private final Object lock;
+
+    Rename(OperationExecutorState state, List<Slice> params) {
+        super(state.base(), params);
+        this.lock = state.lock();
     }
 
     private boolean rename(Slice key, Slice newKey) {
@@ -25,6 +28,8 @@ class Rename extends AbstractRedisOperation {
         base().deleteValue(newKey);
         base().putValue(newKey, value, ttl);
         base().deleteValue(key);
+        lock.notifyAll();
+
         return true;
     }
 
diff --git a/src/main/java/com/github/fppt/jedismock/operations/lists/BPop.java b/src/main/java/com/github/fppt/jedismock/operations/lists/BPop.java
index 5f4e485..9f9d82f 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/lists/BPop.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/lists/BPop.java
@@ -1,8 +1,9 @@
 package com.github.fppt.jedismock.operations.lists;
 
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.exception.WrongValueTypeException;
 import com.github.fppt.jedismock.operations.AbstractRedisOperation;
 import com.github.fppt.jedismock.server.Response;
-import com.github.fppt.jedismock.datastructures.Slice;
 import com.github.fppt.jedismock.server.SliceParser;
 import com.github.fppt.jedismock.storage.OperationExecutorState;
 
@@ -10,15 +11,17 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
-import static com.github.fppt.jedismock.Utils.convertToLong;
+import static com.github.fppt.jedismock.Utils.convertToDouble;
 
 abstract class BPop extends AbstractRedisOperation {
 
     private final Object lock;
+    private final boolean isInTransaction;
 
     BPop(OperationExecutorState state, List<Slice> params) {
         super(state.base(), params);
         this.lock = state.lock();
+        this.isInTransaction = state.isTransactionModeOn();
     }
 
     abstract AbstractRedisOperation popper(List<Slice> params);
@@ -29,14 +32,22 @@ abstract class BPop extends AbstractRedisOperation {
             throw new IndexOutOfBoundsException("require at least 2 params");
         }
         List<Slice> keys = params().subList(0, size - 1);
-        long timeout = convertToLong(params().get(size - 1).toString());
-        Slice source = getKey(keys);
-        long waitEnd = System.nanoTime() + timeout * 1_000_000_000L;
-        long waitTime;
+        long timeoutNanos = (long) (convertToDouble(params().get(size - 1).toString()) * 1_000_000_000L);
+
+        if (timeoutNanos < 0) {
+            throw new IllegalArgumentException("ERR timeout is negative");
+        }
+
+        Slice source = getKey(keys, true);
+
+        long waitEnd = System.nanoTime() + timeoutNanos;
+        long waitTimeNanos;
         try {
-            while (source == null && (waitTime = (waitEnd - System.nanoTime()) / 1_000_000L) > 0) {
-                lock.wait(waitTime);
-                source = getKey(keys);
+            while (source == null &&
+                    !isInTransaction &&
+                    (waitTimeNanos = timeoutNanos == 0 ? 0 : waitEnd - System.nanoTime()) >= 0) {
+                lock.wait(waitTimeNanos / 1_000_000, (int) waitTimeNanos % 1_000_000);
+                source = getKey(keys, false);
             }
         } catch (InterruptedException e) {
             //wait interrupted prematurely
@@ -52,9 +63,20 @@ abstract class BPop extends AbstractRedisOperation {
         }
     }
 
-    private Slice getKey(List<Slice> list) {
+    private Slice getKey(List<Slice> list, boolean checkForType) {
         for (Slice key : list) {
-            Slice result = new LLen(base(), Collections.singletonList(key)).execute();
+            if (!base().exists(key)) {
+                continue;
+            }
+            Slice result;
+            try {
+                result = new LLen(base(), Collections.singletonList(key)).execute();
+            } catch (WrongValueTypeException e) {
+                if (checkForType) {
+                    throw e;
+                }
+                continue;
+            }
             int length = SliceParser.consumeInteger(result.data());
             if (length > 0) {
                 return key;
diff --git a/src/main/java/com/github/fppt/jedismock/operations/lists/BRPopLPush.java b/src/main/java/com/github/fppt/jedismock/operations/lists/BRPopLPush.java
index 6675751..b469a38 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/lists/BRPopLPush.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/lists/BRPopLPush.java
@@ -1,35 +1,44 @@
 package com.github.fppt.jedismock.operations.lists;
 
+import com.github.fppt.jedismock.datastructures.Slice;
 import com.github.fppt.jedismock.operations.RedisCommand;
-import com.github.fppt.jedismock.storage.OperationExecutorState;
 import com.github.fppt.jedismock.server.Response;
-import com.github.fppt.jedismock.datastructures.Slice;
 import com.github.fppt.jedismock.server.SliceParser;
+import com.github.fppt.jedismock.storage.OperationExecutorState;
 
 import java.util.Arrays;
 import java.util.List;
 
-import static com.github.fppt.jedismock.Utils.convertToLong;
+import static com.github.fppt.jedismock.Utils.convertToDouble;
 
 @RedisCommand("brpoplpush")
 class BRPopLPush extends RPopLPush {
     private long count = 0L;
     private final Object lock;
+    private final boolean isInTransaction;
 
     BRPopLPush(OperationExecutorState state, List<Slice> params) {
         super(state, params);
         this.lock = state.lock();
+        this.isInTransaction = state.isTransactionModeOn();
     }
 
-    protected void doOptionalWork(){
+    protected void doOptionalWork() {
         Slice source = params().get(0);
-        long timeout = convertToLong(params().get(2).toString());
-        long waitEnd = System.nanoTime() + timeout * 1_000_000_000L;
-        long waitTime;
+        long timeoutNanos = (long) (convertToDouble(params().get(2).toString()) * 1_000_000_000L);
+
+        if (timeoutNanos < 0) {
+            throw new IllegalArgumentException("ERR timeout is negative");
+        }
+
+        long waitEnd = System.nanoTime() + timeoutNanos;
+        long waitTimeNanos;
         count = getCount(source);
         try {
-            while (count == 0L && (waitTime = (waitEnd - System.nanoTime()) / 1_000_000L) > 0) {
-                lock.wait(waitTime);
+            while (count == 0L &&
+                    !isInTransaction &&
+                    (waitTimeNanos = timeoutNanos == 0 ? 0 : waitEnd - System.nanoTime()) >= 0) {
+                lock.wait(waitTimeNanos / 1_000_000, (int) waitTimeNanos % 1_000_000);
                 count = getCount(source);
             }
         } catch (InterruptedException e) {
@@ -39,14 +48,14 @@ class BRPopLPush extends RPopLPush {
     }
 
     protected Slice response() {
-        if(count != 0){
+        if (count != 0) {
             return super.response();
         } else {
             return Response.NULL;
         }
     }
 
-    private long getCount(Slice source){
+    private long getCount(Slice source) {
         Slice index = Slice.create("0");
         List<Slice> commands = Arrays.asList(source, index, index);
         Slice result = new LRange(base(), commands).execute();
diff --git a/src/main/java/com/github/fppt/jedismock/operations/lists/LInsert.java b/src/main/java/com/github/fppt/jedismock/operations/lists/LInsert.java
new file mode 100644
index 0000000..0b5f0f2
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/lists/LInsert.java
@@ -0,0 +1,48 @@
+package com.github.fppt.jedismock.operations.lists;
+
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.operations.AbstractRedisOperation;
+import com.github.fppt.jedismock.operations.RedisCommand;
+import com.github.fppt.jedismock.server.Response;
+import com.github.fppt.jedismock.storage.RedisBase;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RedisCommand("linsert")
+public class LInsert extends AbstractRedisOperation {
+    private static final String BEFORE = "BEFORE";
+    private static final String AFTER = "AFTER";
+
+    public LInsert(RedisBase base, List<Slice> params) {
+        super(base, params);
+    }
+
+    @Override
+    protected Slice response() {
+        Slice key = params().get(0);
+        String direction = params().get(1).toString();
+
+        if (!Arrays.asList(BEFORE, AFTER).contains(direction.toUpperCase())) {
+            throw new IllegalArgumentException("ERR syntax error");
+        }
+
+        Slice pivot = params().get(2);
+        Slice element = params().get(3);
+
+        if (!base().exists(key)) {
+            return Response.integer(0);
+        }
+
+        List<Slice> storedElements = base().getList(key).getStoredData();
+        int i = storedElements.indexOf(pivot);
+
+        if (i == -1) {
+            return Response.integer(-1);
+        }
+
+        int index = direction.equalsIgnoreCase(BEFORE) ? i : i + 1;
+        storedElements.add(index, element);
+        return Response.integer(storedElements.size());
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/lists/LPos.java b/src/main/java/com/github/fppt/jedismock/operations/lists/LPos.java
new file mode 100644
index 0000000..e0903a7
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/lists/LPos.java
@@ -0,0 +1,110 @@
+package com.github.fppt.jedismock.operations.lists;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import com.github.fppt.jedismock.datastructures.RMList;
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.operations.AbstractRedisOperation;
+import com.github.fppt.jedismock.operations.RedisCommand;
+import com.github.fppt.jedismock.server.Response;
+import com.github.fppt.jedismock.storage.RedisBase;
+
+import static java.lang.Math.abs;
+
+@RedisCommand("lpos")
+public class LPos extends AbstractRedisOperation {
+
+    private static final String RANK = "rank";
+    private static final String COUNT = "count";
+    private static final String MAXLEN = "maxlen";
+
+    private int count;
+    private int rank;
+    private int maxLen;
+
+    private boolean containsCount = false;
+
+    public LPos(RedisBase base, List<Slice> params) {
+        super(base, params);
+    }
+
+    @Override
+    protected Slice response() {
+        Slice key = params().get(0);
+        Slice element = params().get(1);
+
+        RMList list = getListFromBaseOrCreateEmpty(key);
+
+        List<Slice> storedData = list.getStoredData();
+
+        parseArgs(storedData.size());
+        List<Slice> result = new LinkedList<>();
+        int matches = 0;
+
+        int start = rank > 0 ? 0 : storedData.size() - 1;
+        int add = rank > 0 ? 1 : -1;
+
+        int checkedValues = 0;
+
+        for (int i = start; i < storedData.size() && i >= 0; i += add) {
+
+            if (maxLen <= checkedValues) {
+                break;
+            }
+            ++checkedValues;
+
+            if (!storedData.get(i).equals(element)) {
+                continue;
+            }
+            matches++;
+
+            if (matches >= abs(rank) && result.size() < count) {
+                result.add(Response.integer(i));
+            }
+        }
+
+        if (result.isEmpty()) {
+            return Response.NULL;
+        }
+
+        return !containsCount ? result.get(0) : Response.array(result);
+    }
+
+    private void parseArgs(int listSize) {
+        Map<String, Slice> args = getArgsWithValues(params(), new HashSet<>(Arrays.asList(RANK, COUNT, MAXLEN)));
+
+        rank = Optional.ofNullable(args.get(RANK)).map(slice -> Integer.parseInt(slice.toString())).orElse(1);
+
+        if (rank == 0) {
+            throw new IllegalArgumentException("ERR RANK can't be zero:");
+        }
+
+        count = Optional.ofNullable(args.get(COUNT)).map(slice -> Integer.parseInt(slice.toString())).orElse(1);
+        count = count == 0 ? listSize : count;
+        containsCount = args.containsKey(COUNT);
+        maxLen = Optional.ofNullable(args.get(MAXLEN)).map(slice -> Integer.parseInt(slice.toString())).orElse(0);
+        maxLen = maxLen == 0 ? listSize : maxLen;
+    }
+
+
+    private static Map<String, Slice> getArgsWithValues(List<Slice> params, Set<String> argNames) {
+        Map<String, Slice> argMap = new HashMap<>();
+        for (int i = 0; i < params.size(); i++) {
+            String parameter = params.get(i).toString().toLowerCase();
+            if (argNames.contains(parameter)) {
+                if (++i == params.size()) {
+                    throw new IllegalArgumentException("Param " + parameter + " expected to have a value");
+                }
+                argMap.put(parameter, params.get(i));
+            }
+        }
+        return argMap;
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/lists/LPushX.java b/src/main/java/com/github/fppt/jedismock/operations/lists/LPushX.java
index fdb64f0..78b7f27 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/lists/LPushX.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/lists/LPushX.java
@@ -13,6 +13,7 @@ class LPushX extends LPush {
         super(state, params);
     }
 
+    @Override
     protected Slice response(){
         Slice key = params().get(0);
         boolean exists = base().exists(key);
diff --git a/src/main/java/com/github/fppt/jedismock/operations/lists/LRange.java b/src/main/java/com/github/fppt/jedismock/operations/lists/LRange.java
index 4c3237a..f7bb85e 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/lists/LRange.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/lists/LRange.java
@@ -1,10 +1,10 @@
 package com.github.fppt.jedismock.operations.lists;
 
 import com.github.fppt.jedismock.datastructures.RMList;
+import com.github.fppt.jedismock.datastructures.Slice;
 import com.github.fppt.jedismock.operations.AbstractRedisOperation;
 import com.github.fppt.jedismock.operations.RedisCommand;
 import com.github.fppt.jedismock.server.Response;
-import com.github.fppt.jedismock.datastructures.Slice;
 import com.github.fppt.jedismock.storage.RedisBase;
 
 import java.util.ArrayList;
@@ -34,9 +34,6 @@ class LRange extends AbstractRedisOperation {
         }
         if (end < 0) {
             end = list.size() + end;
-            if (end < 0) {
-                end = 0;
-            }
         }
         List<Slice> result = new ArrayList<>();
         for (int i = start; i <= end && i < list.size(); i++) {
diff --git a/src/main/java/com/github/fppt/jedismock/operations/lists/LSet.java b/src/main/java/com/github/fppt/jedismock/operations/lists/LSet.java
new file mode 100644
index 0000000..06e3e83
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/lists/LSet.java
@@ -0,0 +1,41 @@
+package com.github.fppt.jedismock.operations.lists;
+
+import com.github.fppt.jedismock.Utils;
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.operations.AbstractRedisOperation;
+import com.github.fppt.jedismock.operations.RedisCommand;
+import com.github.fppt.jedismock.server.Response;
+import com.github.fppt.jedismock.storage.RedisBase;
+
+import java.util.List;
+
+@RedisCommand("lset")
+public class LSet extends AbstractRedisOperation {
+    public LSet(RedisBase base, List<Slice> params) {
+        super(base, params);
+    }
+
+    @Override
+    protected Slice response() {
+        Slice key = params().get(0);
+        int index = Utils.convertToInteger(params().get(1).toString());
+        Slice element = params().get(2);
+
+        if (!base().exists(key)) {
+            throw new IllegalArgumentException("ERR no such key");
+        }
+
+        List<Slice> storedData = base().getList(key).getStoredData();
+
+        if (index < 0) {
+            index = storedData.size() + index;
+        }
+
+        if (index < 0 || index >= storedData.size()) {
+            throw new IllegalArgumentException("ERR index out of range");
+        }
+
+        storedData.set(index, element);
+        return Response.OK;
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/lists/ListPopper.java b/src/main/java/com/github/fppt/jedismock/operations/lists/ListPopper.java
index e3c21ab..c8335cb 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/lists/ListPopper.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/lists/ListPopper.java
@@ -11,16 +11,27 @@ import java.util.ArrayList;
 import java.util.List;
 
 abstract class ListPopper extends AbstractRedisOperation {
+
     ListPopper(RedisBase base, List<Slice> params) {
         super(base, params);
     }
 
     abstract Slice popper(List<Slice> list);
 
+    private Slice pop(Slice key, List<Slice> list) {
+        Slice result = popper(list);
+        base().markKeyModified(key);
+        if (list.isEmpty()) {
+            base().deleteValue(key);
+        }
+        return result;
+    }
+
     protected final Slice response() {
         Slice key = params().get(0);
-        final RMList listDBObj = getListFromBaseOrCreateEmpty(key);
-        final List<Slice> list = listDBObj.getStoredData();
+        RMList listDBObj = getListFromBaseOrCreateEmpty(key);
+        List<Slice> list = listDBObj.getStoredData();
+
         if (list.isEmpty()) return Response.NULL;
         if (params().size() > 1) {
             //Count param
@@ -31,12 +42,12 @@ abstract class ListPopper extends AbstractRedisOperation {
             }
             List<Slice> responseList = new ArrayList<>();
             while (count > 0 && list.size() > 0) {
-                responseList.add(Response.bulkString(popper(list)));
+                responseList.add(Response.bulkString(pop(key, list)));
                 count--;
             }
             return Response.array(responseList);
         } else {
-            return Response.bulkString(popper(list));
+            return Response.bulkString(pop(key, list));
         }
     }
 
diff --git a/src/main/java/com/github/fppt/jedismock/operations/lists/RPopLPush.java b/src/main/java/com/github/fppt/jedismock/operations/lists/RPopLPush.java
index d9e6080..7398a17 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/lists/RPopLPush.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/lists/RPopLPush.java
@@ -1,5 +1,6 @@
 package com.github.fppt.jedismock.operations.lists;
 
+import com.github.fppt.jedismock.datastructures.RMList;
 import com.github.fppt.jedismock.datastructures.Slice;
 import com.github.fppt.jedismock.operations.AbstractRedisOperation;
 import com.github.fppt.jedismock.operations.RedisCommand;
@@ -24,6 +25,11 @@ class RPopLPush extends AbstractRedisOperation {
         Slice source = params().get(0);
         Slice target = params().get(1);
 
+        // check for target type before popping
+        if (base().exists(target) && !(base().getValue(target) instanceof RMList)) {
+            throw new IllegalArgumentException("WRONGTYPE Operation against a key holding the wrong kind of value");
+        }
+
         //Pop last one
         Slice result = new RPop(base(), Collections.singletonList(source)).execute();
         if(result.equals(NULL)) return NULL;
diff --git a/src/main/java/com/github/fppt/jedismock/operations/lists/RPushX.java b/src/main/java/com/github/fppt/jedismock/operations/lists/RPushX.java
new file mode 100644
index 0000000..72b55f4
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/lists/RPushX.java
@@ -0,0 +1,27 @@
+package com.github.fppt.jedismock.operations.lists;
+
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.operations.RedisCommand;
+import com.github.fppt.jedismock.server.Response;
+import com.github.fppt.jedismock.storage.OperationExecutorState;
+
+import java.util.List;
+
+@RedisCommand("rpushx")
+public class RPushX extends RPush {
+    RPushX(OperationExecutorState state, List<Slice> params) {
+        super(state, params);
+    }
+
+    @Override
+    protected Slice response() {
+        Slice key = params().get(0);
+        boolean exists = base().exists(key);
+
+        if (exists) {
+            return super.response();
+        }
+
+        return Response.integer(0);
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/lists/Sort.java b/src/main/java/com/github/fppt/jedismock/operations/lists/Sort.java
new file mode 100644
index 0000000..20d8a31
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/lists/Sort.java
@@ -0,0 +1,118 @@
+package com.github.fppt.jedismock.operations.lists;
+
+import com.github.fppt.jedismock.Utils;
+import com.github.fppt.jedismock.datastructures.RMDataStructure;
+import com.github.fppt.jedismock.datastructures.RMList;
+import com.github.fppt.jedismock.datastructures.RMSet;
+import com.github.fppt.jedismock.datastructures.RMZSet;
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.datastructures.ZSetEntry;
+import com.github.fppt.jedismock.exception.WrongValueTypeException;
+import com.github.fppt.jedismock.operations.AbstractRedisOperation;
+import com.github.fppt.jedismock.operations.RedisCommand;
+import com.github.fppt.jedismock.server.Response;
+import com.github.fppt.jedismock.storage.OperationExecutorState;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * This sort implementation currently does not implement parameters BY and GET
+ * moreover the complexity is currently O(N*log(N)) where N is the number of elements in the list
+ */
+@RedisCommand("sort")
+public class Sort extends AbstractRedisOperation {
+    private static final String LIMIT_PARAM = "LIMIT";
+    private static final String ALPHA_PARAM = "ALPHA";
+    private static final String STORE_PARAM = "STORE";
+    private static final String DESC_PARAM = "DESC";
+
+    private final Object lock;
+
+    private boolean sortNumerically = true;
+    private Slice storeTo = null;
+    private int offset = 0;
+    private int count = Integer.MAX_VALUE;
+    private int compareMultiplier = 1;
+
+    public Sort(OperationExecutorState state, List<Slice> params) {
+        super(state.base(), params);
+        this.lock = state.lock();
+    }
+
+    @Override
+    protected Slice response() {
+        Slice key = params().get(0);
+        parseArgs();
+
+        Slice[] items = getItems(key);
+
+        try {
+            Arrays.sort(items, this::compare);
+        } catch (WrongValueTypeException e) {
+            throw new WrongValueTypeException("ERR One or more scores can't be converted into double");
+        }
+
+        List<Slice> sorted = Arrays.stream(items)
+                .skip(offset)
+                .limit(count)
+                .collect(Collectors.toList());
+
+        if (storeTo != null) {
+            base().putValue(storeTo, new RMList(sorted));
+            lock.notifyAll();
+            return Response.integer(sorted.size());
+        }
+
+        return Response.array(sorted.stream().map(Response::bulkString).collect(Collectors.toList()));
+    }
+
+    private Slice[] getItems(Slice key) {
+        RMDataStructure dataStructure = base().getValue(key);
+
+        if (dataStructure instanceof RMList) {
+            return ((RMList) dataStructure).getStoredData().toArray(new Slice[0]);
+        }
+
+        if (dataStructure instanceof RMSet) {
+            return ((RMSet) dataStructure).getStoredData().toArray(new Slice[0]);
+        }
+
+        if (dataStructure instanceof RMZSet) {
+            return ((RMZSet) dataStructure).entries(false).stream().map(ZSetEntry::getValue).toArray(Slice[]::new);
+        }
+
+        throw new WrongValueTypeException("WRONGTYPE Operation against a key holding the wrong kind of value");
+    }
+
+    private int compare(Slice a, Slice b) {
+        return (sortNumerically ?
+                Double.compare(Utils.convertToDouble(a.toString()), Utils.convertToDouble(b.toString())) :
+                a.compareTo(b)) * compareMultiplier;
+    }
+
+    private void parseArgs() {
+        List<Slice> params = params();
+        for (int i = 1; i < params.size(); ++i) {
+            if (ALPHA_PARAM.equalsIgnoreCase(params.get(i).toString())) {
+                sortNumerically = false;
+            }
+
+            if (STORE_PARAM.equalsIgnoreCase(params.get(i).toString())) {
+                storeTo = params.get(i + 1);
+                ++i;
+            }
+
+            if (LIMIT_PARAM.equalsIgnoreCase(params.get(i).toString())) {
+                offset = Math.max(Utils.convertToInteger(params.get(i + 1).toString()), 0);
+                count = Utils.convertToInteger(params.get(i + 2).toString());
+                i += 2;
+            }
+
+            if (DESC_PARAM.equalsIgnoreCase(params.get(i).toString())) {
+                compareMultiplier = -1;
+            }
+        }
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/scripting/Eval.java b/src/main/java/com/github/fppt/jedismock/operations/scripting/Eval.java
new file mode 100644
index 0000000..154844e
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/scripting/Eval.java
@@ -0,0 +1,122 @@
+package com.github.fppt.jedismock.operations.scripting;
+
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.operations.AbstractRedisOperation;
+import com.github.fppt.jedismock.operations.RedisCommand;
+import com.github.fppt.jedismock.server.Response;
+import com.github.fppt.jedismock.storage.OperationExecutorState;
+import com.github.fppt.jedismock.storage.RedisBase;
+import org.luaj.vm2.Globals;
+import org.luaj.vm2.LuaError;
+import org.luaj.vm2.LuaTable;
+import org.luaj.vm2.LuaValue;
+import org.luaj.vm2.lib.jse.CoerceJavaToLua;
+import org.luaj.vm2.lib.jse.JsePlatform;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static com.github.fppt.jedismock.operations.scripting.Script.getScriptSHA;
+
+@RedisCommand("eval")
+public class Eval extends AbstractRedisOperation {
+
+    private static final String SCRIPT_PARAM_ERROR = "Wrong number of arguments for EVAL";
+    private static final String SCRIPT_RUNTIME_ERROR = "Error running script (call to function returned nil)";
+    private static final String REDIS_LUA = loadResource();
+    private final Globals globals = JsePlatform.standardGlobals();
+    private final OperationExecutorState state;
+
+    public Eval(final RedisBase base, final List<Slice> params, final OperationExecutorState state)
+            throws NoSuchAlgorithmException {
+        super(base, params);
+        this.state = state;
+    }
+
+    @Override
+    public Slice response() {
+        if (params().size() < 2) {
+            return Response.error(SCRIPT_PARAM_ERROR);
+        }
+
+        final String script = params().get(0).toString();
+
+        this.base().addCachedLuaScript(getScriptSHA(script), script);
+
+        int keysNum = Integer.parseInt(params().get(1).toString());
+        final List<LuaValue> args = getLuaValues(params().subList(2, params().size()));
+
+        globals.set("redis", globals.load(REDIS_LUA).call());
+        globals.set("KEYS", embedLuaListToValue(args.subList(0, keysNum)));
+        globals.set("ARGV", embedLuaListToValue(args.subList(keysNum, args.size())));
+        globals.set("_mock", CoerceJavaToLua.coerce(new LuaRedisCallback(state)));
+        int selected = state.getSelected();
+        try {
+            final LuaValue result = globals.load(script).call();
+            return resolveResult(result);
+        } catch (LuaError e) {
+            return Response.error(String.format("Error running script: %s", e.getMessage()));
+        } finally {
+            state.changeActiveRedisBase(selected);
+        }
+    }
+
+    private static List<LuaValue> getLuaValues(List<Slice> slices) {
+        return slices.stream()
+                .map(Slice::toString)
+                .map(LuaValue::valueOf)
+                .collect(Collectors.toList());
+    }
+
+    public static LuaTable embedLuaListToValue(final List<LuaValue> luaValues) {
+        return LuaValue.listOf(luaValues.toArray(new LuaValue[0]));
+    }
+
+    private Slice resolveResult(LuaValue result) {
+        if (result.isnil()) {
+            return Response.NULL;
+        }
+
+        switch (result.typename()) {
+            case "string":
+                return Response.bulkString(Slice.create(result.tojstring()));
+            case "number":
+                return Response.integer(result.tolong());
+            case "table":
+                if (!result.get("err").isnil()) {
+                    return Response.error(result.get("err").tojstring());
+                }
+                if (!result.get("ok").isnil()) {
+                    return resolveResult(result.get("ok"));
+                }
+                return Response.array(luaTableToList(result));
+            case "boolean":
+                return result.toboolean() ? Response.integer(1) : Response.NULL;
+        }
+        return Response.error(SCRIPT_RUNTIME_ERROR);
+    }
+
+    private ArrayList<Slice> luaTableToList(LuaValue result) {
+        final ArrayList<Slice> list = new ArrayList<>();
+        for (int i = 0; i < result.length(); i++) {
+            list.add(resolveResult(result.get(i + 1)));
+        }
+        return list;
+    }
+
+
+    private static String loadResource() {
+        try (InputStream in = Eval.class.getResourceAsStream("/redis.lua");
+             BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
+            return reader.lines().collect(Collectors.joining("\n"));
+        } catch (IOException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/scripting/EvalSha.java b/src/main/java/com/github/fppt/jedismock/operations/scripting/EvalSha.java
new file mode 100644
index 0000000..74fc56e
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/scripting/EvalSha.java
@@ -0,0 +1,41 @@
+package com.github.fppt.jedismock.operations.scripting;
+
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.operations.AbstractRedisOperation;
+import com.github.fppt.jedismock.operations.RedisCommand;
+import com.github.fppt.jedismock.server.Response;
+import com.github.fppt.jedismock.storage.OperationExecutorState;
+import com.github.fppt.jedismock.storage.RedisBase;
+
+import java.security.NoSuchAlgorithmException;
+import java.util.List;
+
+@RedisCommand("evalsha")
+public class EvalSha extends AbstractRedisOperation {
+    private static final String SCRIPT_PARAM_ERROR = "Wrong number of arguments for EVALSHA";
+
+    private final OperationExecutorState state;
+
+    public EvalSha(final RedisBase base, final List<Slice> params, OperationExecutorState state) {
+        super(base, params);
+        this.state = state;
+    }
+
+    @Override
+    protected Slice response() {
+        if (params().size() < 2) {
+            return Response.error(SCRIPT_PARAM_ERROR);
+        }
+        final String sha = params().get(0).toString();
+        final String script = base().getCachedLuaScript(sha);
+        if (script == null) {
+            return Response.error("NOSCRIPT No matching script. Please use EVAL.");
+        }
+        params().set(0, Slice.create(script));
+        try {
+            return new Eval(base(), params(), state).response();
+        } catch (NoSuchAlgorithmException e) {
+            return Response.error(e.getMessage());
+        }
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/scripting/LuaRedisCallback.java b/src/main/java/com/github/fppt/jedismock/operations/scripting/LuaRedisCallback.java
new file mode 100644
index 0000000..f1cfa6b
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/scripting/LuaRedisCallback.java
@@ -0,0 +1,153 @@
+package com.github.fppt.jedismock.operations.scripting;
+
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.operations.CommandFactory;
+import com.github.fppt.jedismock.operations.RedisOperation;
+import com.github.fppt.jedismock.operations.connection.Select;
+import com.github.fppt.jedismock.storage.OperationExecutorState;
+import org.luaj.vm2.LuaTable;
+import org.luaj.vm2.LuaValue;
+import org.slf4j.LoggerFactory;
+import redis.clients.jedis.exceptions.JedisDataException;
+import redis.clients.jedis.exceptions.JedisNoScriptException;
+import redis.clients.jedis.util.RedisInputStream;
+
+import java.io.ByteArrayInputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static com.github.fppt.jedismock.operations.scripting.Eval.embedLuaListToValue;
+
+public class LuaRedisCallback {
+
+    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(LuaRedisCallback.class);
+    private static final String NOSCRIPT_PREFIX = "NOSCRIPT ";
+
+    private final OperationExecutorState state;
+
+    public LuaRedisCallback(final OperationExecutorState state) {
+        this.state = state;
+    }
+
+    public LuaValue call(LuaValue args) {
+        String operationName = args.get(1).tojstring();
+        List<Slice> a = new ArrayList<>();
+        for (int i = 2; i <= args.length(); i++) {
+            a.add(Slice.create(args.get(i).tojstring()));
+        }
+        return execute(operationName, a);
+    }
+
+    public LuaValue pcall(LuaValue args) {
+        try {
+            return call(args);
+        } catch (final Exception e) {
+            LuaTable errorTable = new LuaTable();
+            errorTable.set(LuaValue.valueOf("err"), LuaValue.valueOf(e.getMessage()));
+            return errorTable;
+        }
+    }
+
+    public String sha1hex(String x) {
+        return Script.getScriptSHA(x);
+    }
+
+    public void log(int level, String message) {
+        LOG.info("redis.log ({}, {})", level, message);
+    }
+
+    private LuaValue execute(final String operationName, final List<Slice> args) {
+
+        final RedisOperation operation =
+                //Specific support for SELECT,
+                //see https://redis.io/docs/manual/programmability/lua-api/#using-selectcommandsselect-inside-scripts
+                "select".equalsIgnoreCase(operationName) ? new Select(state, args) :
+                        CommandFactory.buildOperation(operationName.toLowerCase(), true, state, args);
+        if (operation != null) {
+            throwOnUnsupported(operation);
+            Slice result = operation.execute();
+            if (result == null) {
+                return LuaValue.NONE;
+            } else {
+                byte[] data = result.data();
+                return toLuaValue(new RedisInputStream(new ByteArrayInputStream(data)));
+            }
+        }
+        throw new RuntimeException("Operation not implemented!");
+    }
+
+    private static void throwOnUnsupported(RedisOperation operation) {
+        if (operation.getClass().equals(Eval.class)) {
+            throw new RuntimeException("This Redis command is not allowed from scripts");
+        }
+    }
+
+    private static LuaValue toLuaValue(final RedisInputStream is) {
+        byte b = is.readByte();
+        switch (b) {
+            case '+':
+                return LuaValue.valueOf(processStatusCodeReply(is));
+            case '$':
+                return LuaValue.valueOf(processBulkReply(is));
+            case '*':
+                return embedLuaListToValue(processMultiBulkReply(is));
+            case ':':
+                return LuaValue.valueOf(processInteger(is));
+            case '-':
+                String message = is.readLine();
+                if (message.startsWith(NOSCRIPT_PREFIX)) {
+                    throw new JedisNoScriptException(message);
+                } else {
+                    throw new JedisDataException(message);
+                }
+            default:
+                return LuaValue.NONE;
+        }
+
+    }
+
+    private static byte[] processStatusCodeReply(RedisInputStream is) {
+        return is.readLineBytes();
+    }
+
+    private static byte[] processBulkReply(RedisInputStream is) {
+        int len = is.readIntCrLf();
+        if (len <= 0) {
+            return new byte[0];
+        } else {
+            byte[] read = new byte[len];
+            int size;
+            for (int offset = 0; offset < len; offset += size) {
+                size = is.read(read, offset, len - offset);
+                if (size == -1) {
+                    throw new RuntimeException("It seems like server has closed the connection.");
+                }
+            }
+            is.readByte();
+            is.readByte();
+            return read;
+        }
+    }
+
+    private static Long processInteger(RedisInputStream is) {
+        return is.readLongCrLf();
+    }
+
+    private static List<LuaValue> processMultiBulkReply(RedisInputStream is) {
+        int num = is.readIntCrLf();
+        if (num <= 0) {
+            return Collections.emptyList();
+        } else {
+            List<LuaValue> ret = new ArrayList<>(num);
+            for (int i = 0; i < num; ++i) {
+                try {
+                    ret.add(toLuaValue(is));
+                } catch (JedisDataException e) {
+                    System.err.println(e.getMessage());
+                }
+            }
+            return ret;
+        }
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/scripting/Script.java b/src/main/java/com/github/fppt/jedismock/operations/scripting/Script.java
new file mode 100644
index 0000000..80b1b6a
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/scripting/Script.java
@@ -0,0 +1,73 @@
+package com.github.fppt.jedismock.operations.scripting;
+
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.operations.AbstractRedisOperation;
+import com.github.fppt.jedismock.operations.RedisCommand;
+import com.github.fppt.jedismock.server.Response;
+import com.github.fppt.jedismock.storage.RedisBase;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.List;
+
+@RedisCommand("script")
+public class Script extends AbstractRedisOperation {
+    private static final String SCRIPT_PARAM_ERROR = "Wrong number of arguments for SCRIPT";
+
+    public Script(final RedisBase base, final List<Slice> params) {
+        super(base, params);
+    }
+
+    @Override
+    protected Slice response() {
+        final String operation = params().get(0).toString();
+        if ("LOAD".equalsIgnoreCase(operation)) {
+            return load();
+        } else if ("EXISTS".equalsIgnoreCase(operation)) {
+            return exists();
+        } else if ("FLUSH".equalsIgnoreCase(operation)) {
+            base().flushCachedLuaScrips();
+            return Response.OK;
+        }
+        return Response.error(String.format("Unsupported operation: script %s", operation));
+    }
+
+    private Slice exists() {
+        List<Slice> result = new ArrayList<>();
+        for (int i = 1; i < params().size(); i++) {
+            result.add(
+                    Response.integer(
+                            base().cachedLuaScriptExists(params().get(i).toString()) ? 1 : 0));
+        }
+        return Response.array(result);
+    }
+
+    private Slice load() {
+        if (params().size() < 2) {
+            return Response.error(SCRIPT_PARAM_ERROR);
+        }
+        final String script = params().get(1).toString();
+        final String scriptSHA = getScriptSHA(script);
+        base().addCachedLuaScript(scriptSHA, script);
+        return Response.bulkString(Slice.create(scriptSHA));
+    }
+
+    public static String getScriptSHA(final String script) {
+        try {
+            final MessageDigest sha1 = MessageDigest.getInstance("SHA1");
+            final byte[] scriptSHA1 = sha1.digest(script.getBytes());
+            return byteArrayToHexString(scriptSHA1);
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static String byteArrayToHexString(byte[] b) {
+        StringBuilder result = new StringBuilder();
+        for (byte value : b) {
+            result.append(Integer.toString((value & 0xff) + 0x100, 16).substring(1));
+        }
+        return result.toString();
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/sets/SDiff.java b/src/main/java/com/github/fppt/jedismock/operations/sets/SDiff.java
new file mode 100644
index 0000000..767ce2c
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/sets/SDiff.java
@@ -0,0 +1,41 @@
+package com.github.fppt.jedismock.operations.sets;
+
+import com.github.fppt.jedismock.datastructures.RMSet;
+import com.github.fppt.jedismock.operations.AbstractRedisOperation;
+import com.github.fppt.jedismock.operations.RedisCommand;
+import com.github.fppt.jedismock.server.Response;
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.storage.RedisBase;
+
+import java.util.List;
+import java.util.Set;
+
+import static java.util.stream.Collectors.toList;
+
+import java.util.HashSet;
+
+@RedisCommand("sdiff")
+class SDiff extends AbstractRedisOperation {
+    SDiff(RedisBase base, List<Slice> params) {
+        super(base, params);
+    }
+
+    final Set<Slice> getDifference() {
+        Slice key = params().get(0);
+        RMSet setObj = getSetFromBaseOrCreateEmpty(key);
+        Set<Slice> result = new HashSet<>(setObj.getStoredData());
+
+        for(int i = 1; i < params().size(); i++){
+            RMSet secondSetObj = getSetFromBaseOrCreateEmpty(params().get(i));
+            Set<Slice> secondSet = secondSetObj.getStoredData();
+            result.removeAll(secondSet);
+        }
+
+        return result;
+    }
+
+    @Override
+    protected Slice response() {
+        return Response.array(getDifference().stream().map(Response::bulkString).collect(toList()));
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/sets/SDiffStore.java b/src/main/java/com/github/fppt/jedismock/operations/sets/SDiffStore.java
new file mode 100644
index 0000000..aa9ba73
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/sets/SDiffStore.java
@@ -0,0 +1,16 @@
+package com.github.fppt.jedismock.operations.sets;
+
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.operations.RedisCommand;
+import com.github.fppt.jedismock.storage.RedisBase;
+
+import java.util.List;
+
+
+@RedisCommand("sdiffstore")
+class SDiffStore extends SStore {
+    SDiffStore(RedisBase base, List<Slice> params) {
+        super(base, params,
+                (b, p) -> new SDiff(b, p).getDifference());
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/sets/SInter.java b/src/main/java/com/github/fppt/jedismock/operations/sets/SInter.java
index d28578a..afa4774 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/sets/SInter.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/sets/SInter.java
@@ -12,24 +12,30 @@ import java.util.Set;
 
 import static java.util.stream.Collectors.toList;
 
+import java.util.HashSet;
+
 @RedisCommand("sinter")
 class SInter extends AbstractRedisOperation {
     SInter(RedisBase base, List<Slice> params) {
         super(base, params);
     }
 
-    @Override
-    protected Slice response() {
+    final Set<Slice> getIntersection() {
         Slice key = params().get(0);
         RMSet setObj = getSetFromBaseOrCreateEmpty(key);
-        Set<Slice> resultSoFar = setObj.getStoredData();
+        Set<Slice> result = new HashSet<>(setObj.getStoredData());
 
         for(int i = 1; i < params().size(); i++){
             RMSet secondSetObj = getSetFromBaseOrCreateEmpty(params().get(i));
             Set<Slice> secondSet = secondSetObj.getStoredData();
-            resultSoFar.retainAll(secondSet);
+            result.retainAll(secondSet);
         }
 
-        return Response.array(resultSoFar.stream().map(Response::bulkString).collect(toList()));
+        return result;
+    }
+
+    @Override
+    protected Slice response() {
+        return Response.array(getIntersection().stream().map(Response::bulkString).collect(toList()));
     }
 }
diff --git a/src/main/java/com/github/fppt/jedismock/operations/sets/SInterStore.java b/src/main/java/com/github/fppt/jedismock/operations/sets/SInterStore.java
new file mode 100644
index 0000000..61f702e
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/sets/SInterStore.java
@@ -0,0 +1,16 @@
+package com.github.fppt.jedismock.operations.sets;
+
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.operations.RedisCommand;
+import com.github.fppt.jedismock.storage.RedisBase;
+
+import java.util.List;
+
+
+@RedisCommand("sinterstore")
+class SInterStore extends SStore {
+    SInterStore(RedisBase base, List<Slice> params) {
+        super(base, params,
+                (b, p) -> new SInter(b, p).getIntersection());
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/sets/SMove.java b/src/main/java/com/github/fppt/jedismock/operations/sets/SMove.java
new file mode 100644
index 0000000..88cb694
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/sets/SMove.java
@@ -0,0 +1,37 @@
+package com.github.fppt.jedismock.operations.sets;
+
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.exception.WrongValueTypeException;
+import com.github.fppt.jedismock.operations.AbstractRedisOperation;
+import com.github.fppt.jedismock.operations.RedisCommand;
+import com.github.fppt.jedismock.server.Response;
+import com.github.fppt.jedismock.storage.RedisBase;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RedisCommand("smove")
+public class SMove extends AbstractRedisOperation {
+    SMove(RedisBase base, List<Slice> params) {
+        super(base, params);
+    }
+
+    @Override
+    protected Slice response() {
+        Slice src = params().get(0);
+        Slice dest = params().get(1);
+        Slice member = params().get(2);
+
+        // check destination type BEFORE deleting from src
+        if (base().getValue(dest) != null && base().getSet(dest) == null) {
+            throw new WrongValueTypeException("WRONGTYPE dest is not a set");
+        }
+
+        final int result = new SRem(base(), Arrays.asList(src, member)).remove();
+
+        if (result > 0 && !getSetFromBaseOrCreateEmpty(dest).getStoredData().contains(member)) {
+            new SAdd(base(), Arrays.asList(dest, member)).execute();
+        }
+        return Response.integer(result);
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/sets/SPop.java b/src/main/java/com/github/fppt/jedismock/operations/sets/SPop.java
index 2567732..13c18d6 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/sets/SPop.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/sets/SPop.java
@@ -7,29 +7,44 @@ import com.github.fppt.jedismock.server.Response;
 import com.github.fppt.jedismock.storage.RedisBase;
 import com.github.fppt.jedismock.datastructures.Slice;
 
+import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
 
+import static com.github.fppt.jedismock.Utils.convertToInteger;
+
 @RedisCommand("spop")
 class SPop extends AbstractRedisOperation {
-    SPop(RedisBase base, List<Slice> params ) {
+    SPop(RedisBase base, List<Slice> params) {
         super(base, params);
     }
 
-    Slice popper(Set<Slice> collection) {
+    private List<Slice> popper(Set<Slice> collection, int number) {
+        List<Slice> result = new ArrayList<>();
         Iterator<Slice> it = collection.iterator();
-        Slice v = it.next();
-        it.remove();
-        return v;
+        while (number > 0 && it.hasNext()) {
+            Slice v = it.next();
+            result.add(Response.bulkString(v));
+            it.remove();
+            number--;
+        }
+        return result;
     }
 
     protected Slice response() {
         Slice key = params().get(0);
+        int number = params().size() > 1 ?
+                convertToInteger(params().get(1).toString()) : 1;
+
         final RMSet setDBObj = getSetFromBaseOrCreateEmpty(key);
         Set<Slice> data = setDBObj.getStoredData();
-        if(data == null || data.isEmpty()) return Response.NULL;
-        Slice v = popper(data);
-        return Response.bulkString(v);
+        if (data == null || data.isEmpty()) return Response.NULL;
+        List<Slice> v = popper(data, number);
+        if (v.size() == 1) {
+            return v.get(0);
+        } else {
+            return Response.array(v);
+        }
     }
 }
diff --git a/src/main/java/com/github/fppt/jedismock/operations/sets/SRandMember.java b/src/main/java/com/github/fppt/jedismock/operations/sets/SRandMember.java
new file mode 100644
index 0000000..87d2974
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/sets/SRandMember.java
@@ -0,0 +1,59 @@
+package com.github.fppt.jedismock.operations.sets;
+
+import com.github.fppt.jedismock.datastructures.RMSet;
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.operations.AbstractRedisOperation;
+import com.github.fppt.jedismock.operations.RedisCommand;
+import com.github.fppt.jedismock.server.Response;
+import com.github.fppt.jedismock.storage.RedisBase;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.stream.Collectors;
+
+import static com.github.fppt.jedismock.Utils.convertToInteger;
+
+@RedisCommand("srandmember")
+public class SRandMember extends AbstractRedisOperation {
+    public SRandMember(RedisBase base, List<Slice> params) {
+        super(base, params);
+    }
+
+    @Override
+    protected Slice response() {
+        Slice key = params().get(0);
+        RMSet set = base().getSet(key);
+        if (set == null) {
+            return Response.NULL;
+        }
+        int number;
+        if (params().size() > 1) {
+            number = convertToInteger(params().get(1).toString());
+        } else {
+            number = 1;
+        }
+        //TODO: more effective algorithms should be used here,
+        //avoiding conversion of set to list, shuffling all the elements etc.
+        List<Slice> list = new ArrayList<>(set.getStoredData());
+        if (number == 1) {
+            int index = ThreadLocalRandom.current().nextInt(list.size());
+            return Response.bulkString(list.get(index));
+        } else if (number > 1) {
+            Collections.shuffle(list);
+            return Response.array(
+                    list.stream()
+                            .map(Response::bulkString)
+                            .limit(number)
+                            .collect(Collectors.toList()));
+        } else {
+            List<Slice> result =
+                    ThreadLocalRandom.current().ints(-number, 0, list.size())
+                    .mapToObj(list::get)
+                    .map(Response::bulkString)
+                    .collect(Collectors.toList());
+            return Response.array(result);
+        }
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/sets/SRem.java b/src/main/java/com/github/fppt/jedismock/operations/sets/SRem.java
index 927afed..e9d50d5 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/sets/SRem.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/sets/SRem.java
@@ -18,17 +18,26 @@ class SRem extends AbstractRedisOperation {
         super(base, params);
     }
 
-    protected Slice response() {
+    final int remove() {
         Slice key = params().get(0);
         RMSet setDBObj = getSetFromBaseOrCreateEmpty(key);
         Set<Slice> set = setDBObj.getStoredData();
-        if(set == null || set.isEmpty()) return Response.integer(0);
+        if (set == null) {
+            return 0;
+        }
         int count = 0;
         for (int i = 1; i < params().size(); i++) {
             if (set.remove(params().get(i))) {
                 count++;
             }
         }
-        return Response.integer(count);
+        if (set.isEmpty()) {
+            base().deleteValue(key);
+        }
+        return count;
+    }
+
+    protected Slice response() {
+        return Response.integer(remove());
     }
 }
diff --git a/src/main/java/com/github/fppt/jedismock/operations/sets/SStore.java b/src/main/java/com/github/fppt/jedismock/operations/sets/SStore.java
new file mode 100644
index 0000000..2a78001
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/sets/SStore.java
@@ -0,0 +1,34 @@
+package com.github.fppt.jedismock.operations.sets;
+
+import com.github.fppt.jedismock.datastructures.RMSet;
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.operations.AbstractRedisOperation;
+import com.github.fppt.jedismock.server.Response;
+import com.github.fppt.jedismock.storage.RedisBase;
+
+import java.util.List;
+import java.util.Set;
+import java.util.function.BiFunction;
+
+public abstract class SStore extends AbstractRedisOperation {
+    private final BiFunction<RedisBase, List<Slice>, Set<Slice>> operation;
+
+    public SStore(RedisBase base,
+                  List<Slice> params,
+                  BiFunction<RedisBase, List<Slice>, Set<Slice>> operation) {
+        super(base, params);
+        this.operation = operation;
+    }
+
+    @Override
+    protected final Slice response() {
+        Slice key = params().get(0);
+        Set<Slice> result = operation.apply(base(), params().subList(1, params().size()));
+        if (result.isEmpty()) {
+            base().deleteValue(key);
+        } else {
+            base().putValue(key, new RMSet(result));
+        }
+        return Response.integer(result.size());
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/sets/SUnion.java b/src/main/java/com/github/fppt/jedismock/operations/sets/SUnion.java
new file mode 100644
index 0000000..1494cf7
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/sets/SUnion.java
@@ -0,0 +1,41 @@
+package com.github.fppt.jedismock.operations.sets;
+
+import com.github.fppt.jedismock.datastructures.RMSet;
+import com.github.fppt.jedismock.operations.AbstractRedisOperation;
+import com.github.fppt.jedismock.operations.RedisCommand;
+import com.github.fppt.jedismock.server.Response;
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.storage.RedisBase;
+
+import java.util.List;
+import java.util.Set;
+
+import static java.util.stream.Collectors.toList;
+
+import java.util.HashSet;
+
+@RedisCommand("sunion")
+class SUnion extends AbstractRedisOperation {
+    SUnion(RedisBase base, List<Slice> params) {
+        super(base, params);
+    }
+
+    final Set<Slice> getUnion() {
+        Slice key = params().get(0);
+        RMSet setObj = getSetFromBaseOrCreateEmpty(key);
+        Set<Slice> resultSoFar = new HashSet<>(setObj.getStoredData());
+
+        for(int i = 1; i < params().size(); i++){
+            RMSet secondSetObj = getSetFromBaseOrCreateEmpty(params().get(i));
+            Set<Slice> secondSet = secondSetObj.getStoredData();
+            resultSoFar.addAll(secondSet);
+        }
+
+        return resultSoFar;
+    }
+
+    @Override
+    protected Slice response() {
+        return Response.array(getUnion().stream().map(Response::bulkString).collect(toList()));
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/sets/SUnionStore.java b/src/main/java/com/github/fppt/jedismock/operations/sets/SUnionStore.java
new file mode 100644
index 0000000..2541d03
--- /dev/null
+++ b/src/main/java/com/github/fppt/jedismock/operations/sets/SUnionStore.java
@@ -0,0 +1,16 @@
+package com.github.fppt.jedismock.operations.sets;
+
+import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.operations.RedisCommand;
+import com.github.fppt.jedismock.storage.RedisBase;
+
+import java.util.List;
+
+
+@RedisCommand("sunionstore")
+class SUnionStore extends SStore {
+    SUnionStore(RedisBase base, List<Slice> params) {
+        super(base, params,
+                (b, p) -> new SUnion(b, p).getUnion());
+    }
+}
diff --git a/src/main/java/com/github/fppt/jedismock/operations/sortedsets/AbstractByScoreOperation.java b/src/main/java/com/github/fppt/jedismock/operations/sortedsets/AbstractByScoreOperation.java
index 7a01047..527de14 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/sortedsets/AbstractByScoreOperation.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/sortedsets/AbstractByScoreOperation.java
@@ -75,7 +75,10 @@ public abstract class AbstractByScoreOperation extends AbstractRedisOperation {
             } else if ("limit".equalsIgnoreCase(param)) {
                 long offset = convertToLong(params().get(++i).toString());
                 long count = convertToLong(params().get(++i).toString());
-                entries = entries.skip(offset).limit(count);
+                entries = entries.skip(offset);
+                if (count >= 0) {
+                    entries = entries.limit(count);
+                }
             }
         }
 
diff --git a/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZRangeByLex.java b/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZRangeByLex.java
index b151e65..47d0032 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZRangeByLex.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZRangeByLex.java
@@ -11,7 +11,11 @@ import com.github.fppt.jedismock.storage.RedisBase;
 
 import java.util.Collections;
 import java.util.List;
+import java.util.NavigableSet;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static com.github.fppt.jedismock.Utils.convertToLong;
 
 @RedisCommand("zrangebylex")
 class ZRangeByLex extends AbstractRedisOperation {
@@ -51,9 +55,9 @@ class ZRangeByLex extends AbstractRedisOperation {
 
     private Slice buildErrorResponse(String param) {
         return Response.error("Valid " + param + " must start with '" + INCLUSIVE_PREFIX + "' or '"
-                    + EXCLUSIVE_PREFIX + "' or unbounded");        
+                + EXCLUSIVE_PREFIX + "' or unbounded");
     }
-    
+
     protected boolean validateStart(String start) {
         return NEGATIVELY_INFINITE.equals(start) || startsWithAnyPrefix(start);
     }
@@ -61,18 +65,39 @@ class ZRangeByLex extends AbstractRedisOperation {
     protected boolean validateEnd(String end) {
         return POSITIVELY_INFINITE.equals(end) || startsWithAnyPrefix(end);
     }
-    
+
     protected boolean startsWithAnyPrefix(String s) {
         return s.startsWith(INCLUSIVE_PREFIX) || s.startsWith(EXCLUSIVE_PREFIX);
     }
 
-    protected List<Slice> doProcess(RMZSet map, String start, String end, double score) {
-        return map.subset(buildStartEntryBound(score, start),
-                buildEndEntryBound(score, end))
-                .stream()
+    List<Slice> process(RMZSet map, String start, String end, double score, boolean reverse) {
+        NavigableSet<ZSetEntry> subset = map.subset(buildStartEntryBound(score, start),
+                buildEndEntryBound(score, end));
+        if (reverse) {
+            subset = subset.descendingSet();
+        }
+
+        Stream<Slice> entries = subset.stream()
                 .map(ZSetEntry::getValue)
-                .map(Response::bulkString)
-                .collect(Collectors.toList());
+                .map(Response::bulkString);
+
+        for (int i = 3; i < params().size(); i++) {
+            String param = params().get(i).toString();
+            if ("limit".equalsIgnoreCase(param)) {
+                long offset = convertToLong(params().get(++i).toString());
+                long count = convertToLong(params().get(++i).toString());
+                entries = entries.skip(offset);
+                if (count >= 0) {
+                    entries = entries.limit(count);
+                }
+            }
+        }
+
+        return entries.collect(Collectors.toList());
+    }
+
+    protected List<Slice> doProcess(RMZSet map, String start, String end, double score) {
+        return process(map, start, end, score, false);
     }
 
     protected ZSetEntryBound buildStartEntryBound(double score, String start) {
@@ -83,7 +108,7 @@ class ZRangeByLex extends AbstractRedisOperation {
         }
     }
 
-    protected ZSetEntryBound buildEndEntryBound(double score, String end)  {
+    protected ZSetEntryBound buildEndEntryBound(double score, String end) {
         if (POSITIVELY_INFINITE.equals(end)) {
             return new ZSetEntryBound(score, ZSetEntry.MAX_VALUE, false);
         } else {
@@ -91,7 +116,7 @@ class ZRangeByLex extends AbstractRedisOperation {
         }
     }
 
-    protected String min(){
+    protected String min() {
         return params().get(1).toString();
     }
 
diff --git a/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZRem.java b/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZRem.java
index 317e20f..6c44c36 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZRem.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZRem.java
@@ -28,6 +28,10 @@ class ZRem extends AbstractRedisOperation {
                 count++;
             }
         }
+
+        if (mapDBObj.isEmpty()) {
+            base().deleteValue(key);
+        }
         return Response.integer(count);
     }
 }
diff --git a/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZRemRangeByScore.java b/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZRemRangeByScore.java
index e47cd8d..8eb06ec 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZRemRangeByScore.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZRemRangeByScore.java
@@ -28,7 +28,11 @@ public class ZRemRangeByScore extends AbstractByScoreOperation {
         for (ZSetEntry entry : subset) {
             mapDBObj.remove(entry.getValue());
         }
-        base().putValue(key, mapDBObj);
+        if (mapDBObj.isEmpty()) {
+            base().deleteValue(key);
+        } else {
+            base().putValue(key, mapDBObj);
+        }
         return Response.integer(subset.size());
     }
 }
diff --git a/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZRevRangeByLex.java b/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZRevRangeByLex.java
index f507c98..f491ab1 100644
--- a/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZRevRangeByLex.java
+++ b/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZRevRangeByLex.java
@@ -5,7 +5,6 @@ import com.github.fppt.jedismock.datastructures.Slice;
 import com.github.fppt.jedismock.operations.RedisCommand;
 import com.github.fppt.jedismock.storage.RedisBase;
 
-import java.util.Collections;
 import java.util.List;
 
 @RedisCommand("zrevrangebylex")
@@ -17,9 +16,7 @@ class ZRevRangeByLex extends ZRangeByLex {
 
     @Override
     protected List<Slice> doProcess(RMZSet map, String start, String end, double score) {
-        final List<Slice> list = super.doProcess(map, start, end, score);
-        Collections.reverse(list);
-        return list;
+        return process(map, start, end, score, true);
     }
 
     @Override
diff --git a/src/main/java/com/github/fppt/jedismock/server/RedisClient.java b/src/main/java/com/github/fppt/jedismock/server/RedisClient.java
index 56ed11f..270a4d7 100644
--- a/src/main/java/com/github/fppt/jedismock/server/RedisClient.java
+++ b/src/main/java/com/github/fppt/jedismock/server/RedisClient.java
@@ -1,6 +1,7 @@
 package com.github.fppt.jedismock.server;
 
 import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.exception.EOFException;
 import com.github.fppt.jedismock.storage.OperationExecutorState;
 import com.github.fppt.jedismock.storage.RedisBase;
 import com.github.fppt.jedismock.commands.RedisCommand;
@@ -71,6 +72,9 @@ public class RedisClient implements Runnable {
             return Optional.of(RedisCommandParser.parse(in));
         } catch (ParseErrorException e) {
             return Optional.empty(); // This simply means there is no next command
+        } catch (EOFException e) {
+            close();
+            return Optional.empty();
         }
     }
 
diff --git a/src/main/java/com/github/fppt/jedismock/server/SliceParser.java b/src/main/java/com/github/fppt/jedismock/server/SliceParser.java
index ecc50e1..1dc260d 100644
--- a/src/main/java/com/github/fppt/jedismock/server/SliceParser.java
+++ b/src/main/java/com/github/fppt/jedismock/server/SliceParser.java
@@ -35,11 +35,7 @@ public class SliceParser {
         long ret = 0;
         boolean hasLong = false;
         while (true) {
-            try {
-                c = consumeByte(messageInput);
-            } catch (EOFException e) {
-                throw new ParseErrorException();
-            }
+            c = consumeByte(messageInput);
             if (c == '\r') {
                 break;
             }
@@ -58,24 +54,16 @@ public class SliceParser {
     public static Slice consumeSlice(InputStream messageInput, long len) throws ParseErrorException {
         ByteArrayOutputStream bo = new ByteArrayOutputStream();
         for (long i = 0; i < len; i++) {
-            try {
-                bo.write(consumeByte(messageInput));
-            } catch (EOFException e) {
-                throw new ParseErrorException();
-            }
+            bo.write(consumeByte(messageInput));
         }
         return Slice.create(bo.toByteArray());
     }
 
     public static long consumeCount(InputStream messageInput) throws ParseErrorException {
-        try {
-            expectByte(messageInput, (byte) '*');
-            long count = consumeLong(messageInput);
-            expectByte(messageInput, (byte) '\n');
-            return count;
-        } catch (EOFException e) {
-            throw new ParseErrorException();
-        }
+        expectByte(messageInput, (byte) '*');
+        long count = consumeLong(messageInput);
+        expectByte(messageInput, (byte) '\n');
+        return count;
     }
 
     public static long consumeCount(byte[] message) throws ParseErrorException {
@@ -89,13 +77,9 @@ public class SliceParser {
     }
 
     public static int consumeInteger(InputStream messageInput) throws ParseErrorException {
-        try {
-            expectByte(messageInput, (byte) ':');
-            long count = consumeLong(messageInput);
-            return (int) count;
-        } catch (EOFException e) {
-            throw new ParseErrorException();
-        }
+        expectByte(messageInput, (byte) ':');
+        long count = consumeLong(messageInput);
+        return (int) count;
     }
 
     private static boolean isNumber(byte c) {
@@ -103,17 +87,13 @@ public class SliceParser {
     }
 
     public static Slice consumeParameter(InputStream messageInput) throws ParseErrorException {
-        try {
-            expectByte(messageInput, (byte) '$');
-            long len = consumeLong(messageInput);
-            expectByte(messageInput, (byte) '\n');
-            Slice para = consumeSlice(messageInput, len);
-            expectByte(messageInput, (byte) '\r');
-            expectByte(messageInput, (byte) '\n');
-            return para;
-        } catch (EOFException e) {
-            throw new ParseErrorException();
-        }
+        expectByte(messageInput, (byte) '$');
+        long len = consumeLong(messageInput);
+        expectByte(messageInput, (byte) '\n');
+        Slice para = consumeSlice(messageInput, len);
+        expectByte(messageInput, (byte) '\r');
+        expectByte(messageInput, (byte) '\n');
+        return para;
     }
 
     public static Slice consumeParameter(byte[] message) throws ParseErrorException {
diff --git a/src/main/java/com/github/fppt/jedismock/storage/OperationExecutorState.java b/src/main/java/com/github/fppt/jedismock/storage/OperationExecutorState.java
index 52561a1..c391b0d 100644
--- a/src/main/java/com/github/fppt/jedismock/storage/OperationExecutorState.java
+++ b/src/main/java/com/github/fppt/jedismock/storage/OperationExecutorState.java
@@ -93,4 +93,8 @@ public class OperationExecutorState {
         }
         watchedKeysAffected = false;
     }
+
+    public int getSelected() {
+        return selectedRedisBase;
+    }
 }
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 5899010..1242b65 100644
--- a/src/main/java/com/github/fppt/jedismock/storage/RedisBase.java
+++ b/src/main/java/com/github/fppt/jedismock/storage/RedisBase.java
@@ -1,15 +1,15 @@
 package com.github.fppt.jedismock.storage;
 
 import com.github.fppt.jedismock.Utils;
-import com.github.fppt.jedismock.datastructures.RMHyperLogLog;
-import com.github.fppt.jedismock.datastructures.RMDataStructure;
-import com.github.fppt.jedismock.datastructures.RMSet;
-import com.github.fppt.jedismock.datastructures.RMString;
 import com.github.fppt.jedismock.datastructures.RMBitMap;
+import com.github.fppt.jedismock.datastructures.RMDataStructure;
 import com.github.fppt.jedismock.datastructures.RMHash;
+import com.github.fppt.jedismock.datastructures.RMHyperLogLog;
 import com.github.fppt.jedismock.datastructures.RMList;
-import com.github.fppt.jedismock.datastructures.Slice;
+import com.github.fppt.jedismock.datastructures.RMSet;
+import com.github.fppt.jedismock.datastructures.RMString;
 import com.github.fppt.jedismock.datastructures.RMZSet;
+import com.github.fppt.jedismock.datastructures.Slice;
 import com.github.fppt.jedismock.server.RedisClient;
 
 import java.util.ArrayList;
@@ -31,6 +31,7 @@ public class RedisBase {
             new ExpiringKeyValueStorage(key -> watchedKeys
                     .getOrDefault(key, Collections.emptySet())
                     .forEach(OperationExecutorState::watchedKeyIsAffected));
+    private final Map<String, String> cachedLuaScripts = new HashMap<>();
 
     public Set<Slice> keys() {
         Set<Slice> slices = keyValueStorage.values().keySet();
@@ -298,4 +299,25 @@ public class RedisBase {
             }
         }
     }
+
+    public void markKeyModified(Slice key) {
+        watchedKeys.getOrDefault(key, new HashSet<>()).forEach(OperationExecutorState::watchedKeyIsAffected);
+    }
+
+    public String getCachedLuaScript(String sha1) {
+        return cachedLuaScripts.get(sha1.toLowerCase());
+    }
+
+    public boolean cachedLuaScriptExists(String sha1) {
+        return cachedLuaScripts.containsKey(sha1.toLowerCase());
+    }
+
+    public void flushCachedLuaScrips() {
+        cachedLuaScripts.clear();
+    }
+
+
+    public String addCachedLuaScript(String sha1, String script) {
+        return cachedLuaScripts.put(sha1, script);
+    }
 }
\ No newline at end of file
diff --git a/src/main/resources/redis.lua b/src/main/resources/redis.lua
new file mode 100644
index 0000000..41f0e1a
--- /dev/null
+++ b/src/main/resources/redis.lua
@@ -0,0 +1,32 @@
+-- Global `redis` object available for Lua scripts
+
+return {
+  LOG_DEBUG = 0,
+  LOG_VERBOSE = 1,
+  LOG_NOTICE = 2,
+  LOG_WARNING = 3,
+
+  call = function(...)
+    return _mock:call({...})
+  end,
+
+  pcall = function(...)
+    return _mock:pcall({...})
+  end,
+
+  sha1hex = function(x)
+    return _mock:sha1hex(x)
+  end,
+
+  status_reply = function(x)
+    return { ok = x }
+  end,
+
+  error_reply = function(x)
+      return { err = x }
+  end,
+
+  log = function(level, message)
+      return _mock:log(level, message)
+  end,
+}
diff --git a/supported_operations.md b/supported_operations.md
index 3cf973b..0249db4 100644
--- a/supported_operations.md
+++ b/supported_operations.md
@@ -84,7 +84,7 @@
 :x: restore<br>
 :x: restore-asking<br>
 :heavy_check_mark: scan<br>
-:x: sort<br>
+:heavy_check_mark: sort<br>
 :x: substr<br>
 :x: touch<br>
 :heavy_check_mark: ttl<br>
@@ -100,21 +100,21 @@
 :heavy_check_mark: brpop<br>
 :heavy_check_mark: brpoplpush<br>
 :heavy_check_mark: lindex<br>
-:x: linsert<br>
+:heavy_check_mark: linsert<br>
 :heavy_check_mark: llen<br>
 :x: lmove<br>
 :heavy_check_mark: lpop<br>
-:x: lpos<br>
+:heavy_check_mark: lpos<br>
 :heavy_check_mark: lpush<br>
 :heavy_check_mark: lpushx<br>
 :heavy_check_mark: lrange<br>
 :heavy_check_mark: lrem<br>
-:x: lset<br>
+:heavy_check_mark: lset<br>
 :heavy_check_mark: ltrim<br>
 :heavy_check_mark: rpop<br>
 :heavy_check_mark: rpoplpush<br>
 :heavy_check_mark: rpush<br>
-:x: rpushx<br>
+:heavy_check_mark: rpushx<br>
 
 ## Pub/Sub
 
@@ -127,9 +127,9 @@
 
 ## Scripting
 
-:x: eval<br>
-:x: evalsha<br>
-:x: script<br>
+:heavy_check_mark: eval<br>
+:heavy_check_mark: evalsha<br>
+:heavy_check_mark: script<br>
 
 ## Server
 
@@ -165,20 +165,20 @@
 
 :heavy_check_mark: sadd<br>
 :heavy_check_mark: scard<br>
-:x: sdiff<br>
-:x: sdiffstore<br>
+:heavy_check_mark: sdiff<br>
+:heavy_check_mark: sdiffstore<br>
 :heavy_check_mark: sinter<br>
-:x: sinterstore<br>
+:heavy_check_mark: sinterstore<br>
 :heavy_check_mark: sismember<br>
 :heavy_check_mark: smembers<br>
 :x: smismember<br>
-:x: smove<br>
+:heavy_check_mark: smove<br>
 :heavy_check_mark: spop<br>
-:x: srandmember<br>
+:heavy_check_mark: srandmember<br>
 :heavy_check_mark: srem<br>
 :heavy_check_mark: sscan<br>
-:x: sunion<br>
-:x: sunionstore<br>
+:heavy_check_mark: sunion<br>
+:heavy_check_mark: sunionstore<br>
 
 ## Sorted Sets
 


Test patch:
diff --git a/.github/workflows/native-tests.yml b/.github/workflows/native-tests.yml
index 99d02fb..a2b9961 100644
--- a/.github/workflows/native-tests.yml
+++ b/.github/workflows/native-tests.yml
@@ -12,6 +12,12 @@ jobs:
       - name: Install TCL
         run: sudo apt-get install -y tcl-tls
 
+      - name: Install wait4x
+        run: |
+          curl -#LO https://github.com/atkrad/wait4x/releases/latest/download/wait4x-linux-amd64.tar.gz
+          tar --one-top-level -xvf wait4x-linux-amd64.tar.gz
+          cp ./wait4x-linux-amd64/wait4x /usr/local/bin/wait4x
+
       - name: checkout
         uses: actions/checkout@v2
 
@@ -28,7 +34,7 @@ jobs:
           restore-keys: ${{ runner.os }}-maven-
 
       - name: Fast build and install JedisMock
-        run: mvn -B install -Dmaven.javadoc.skip -DskipTests -Dspotbugs.skip=true -Dcheckstyle.skip=true
+        run: mvn -B install -Dmaven.javadoc.skip -DskipTests -Dspotbugs.skip -Dcheckstyle.skip -Dpmd.skip -Dcpd.skip
 
       - name: Evaluate current JedisMock version
         run: |
@@ -39,10 +45,6 @@ jobs:
         run: mvn -B verify -Djedismock.version=${{ env.jm_version }}
         working-directory: native_tests/jm-test-server
 
-      - name: Run jm-test-server in background
-        run: java -jar target/jm-test-server-1.0-SNAPSHOT-jar-with-dependencies.jar &
-        working-directory: native_tests/jm-test-server
-
       - name: Prepare TCL test run
         run: |
           sudo chmod +x ./gen-test-certs.sh
@@ -50,9 +52,24 @@ jobs:
           ./gen-test-certs.sh
         working-directory: native_tests/linux
 
-      - name: Run native tests
-        run: |
-          ./runtest --host 127.0.0.1 --port 39807 --single unit/type/incr
-          ./runtest --host 127.0.0.1 --port 39807 --single unit/type/hash
-          ./runtest --host 127.0.0.1 --port 39807 --single unit/type/list-2
-        working-directory: native_tests/linux
+      - name: chmod
+        run: chmod +x native_tests/testnative
+
+      - name: incr
+        run: ./testnative unit/type/incr
+        working-directory: native_tests
+
+      - name: hash
+        if: always()
+        run: ./testnative unit/type/hash
+        working-directory: native_tests
+
+      - name: list
+        if: always()
+        run: ./testnative unit/type/list-2 unit/type/list unit/type/list-3
+        working-directory: native_tests
+
+      - name: set
+        if: always()
+        run: ./testnative unit/type/set
+        working-directory: native_tests
diff --git a/native_tests/jm-test-server/src/main/java/com/github/fppt/jedismock/InterceptorMockServer.java b/native_tests/jm-test-server/src/main/java/com/github/fppt/jedismock/InterceptorMockServer.java
index 68fd6de..c1eba25 100644
--- a/native_tests/jm-test-server/src/main/java/com/github/fppt/jedismock/InterceptorMockServer.java
+++ b/native_tests/jm-test-server/src/main/java/com/github/fppt/jedismock/InterceptorMockServer.java
@@ -41,14 +41,16 @@ public final class InterceptorMockServer {
                         // Currently it is supported only for RMHash.
                         // If you will add support for other data structures, insert their handling here
                         if (value instanceof RMHash) {
-                            RMHash hash = (RMHash) value;
-                            return Response.bulkString(Slice.create(hash.getMeta()));
+                            return Response.bulkString(Slice.create("hashtable"));
                         } else if (value instanceof RMString) {
                             return Response.bulkString(Slice.create(" at: "));
                         }
                         return Response.bulkString(
                                 Slice.create("DEBUG OBJECT command for this data structure is not yet supported")
                         );
+                    } else if ("debug".equalsIgnoreCase(roName)
+                            && "reload".equalsIgnoreCase(params.get(0).toString())) {
+                        return Response.OK;
                     } else if ("object".equalsIgnoreCase(roName)
                             && "encoding".equalsIgnoreCase(params.get(0).toString())
                     ) {
@@ -59,7 +61,7 @@ public final class InterceptorMockServer {
                         // If you will add support for other data structures, insert their handling here
                         if (value instanceof RMHash) {
                             RMHash hash = (RMHash) value;
-                            return Response.bulkString(Slice.create(hash.getEncoding()));
+                            return Response.bulkString(Slice.create("hashtable"));
                         }
                         return Response.bulkString(
                                 Slice.create("OBJECT ENCODING command for this data structure is not yet supported")
diff --git a/native_tests/linux/README.md b/native_tests/linux/README.md
index aa28293..bc49391 100644
--- a/native_tests/linux/README.md
+++ b/native_tests/linux/README.md
@@ -15,4 +15,7 @@ When all the conditions are met, run the tests with the following command:
 
     % ./runtest --host <host> --port <port>
     
-For more flexible testing, see [original redis tests](https://github.com/redis/redis/tree/7.0/tests)
\ No newline at end of file
+For more flexible testing, see [original redis tests](https://github.com/redis/redis/tree/7.0/tests)
+
+NOTE: do not forget to comment out the content of `assert_encoding` from `support/test`, as encodings are not 
+supported by JedisMock.
diff --git a/native_tests/linux/tests/support/test.tcl b/native_tests/linux/tests/support/test.tcl
index 23015b3..93d0429 100644
--- a/native_tests/linux/tests/support/test.tcl
+++ b/native_tests/linux/tests/support/test.tcl
@@ -73,8 +73,10 @@ proc assert_error {pattern code} {
 }
 
 proc assert_encoding {enc key} {
-    set dbg [r debug object $key]
-    assert_match "* encoding:$enc *" $dbg
+    # [[JEDIS-MOCK Do nothing as Jedis-mock does not support encodings
+	# set dbg [r debug object $key]
+    # assert_match "* encoding:$enc *" $dbg
+	# JEDIS-MOCK]]
 }
 
 proc assert_type {type key} {
diff --git a/native_tests/linux/tests/unit/type/list.tcl b/native_tests/linux/tests/unit/type/list.tcl
index 5ea2b9c..cb4566d 100644
--- a/native_tests/linux/tests/unit/type/list.tcl
+++ b/native_tests/linux/tests/unit/type/list.tcl
@@ -345,36 +345,36 @@ start_server {
         assert_equal {foo} [r lrange target2 0 -1]
     }
 
-    test "Linked BRPOPLPUSH" {
-      set rd1 [redis_deferring_client]
-      set rd2 [redis_deferring_client]
-
-      r del list1 list2 list3
-
-      $rd1 brpoplpush list1 list2 0
-      $rd2 brpoplpush list2 list3 0
-
-      r rpush list1 foo
-
-      assert_equal {} [r lrange list1 0 -1]
-      assert_equal {} [r lrange list2 0 -1]
-      assert_equal {foo} [r lrange list3 0 -1]
-    }
-
-    test "Circular BRPOPLPUSH" {
-      set rd1 [redis_deferring_client]
-      set rd2 [redis_deferring_client]
-
-      r del list1 list2
-
-      $rd1 brpoplpush list1 list2 0
-      $rd2 brpoplpush list2 list1 0
-
-      r rpush list1 foo
-
-      assert_equal {foo} [r lrange list1 0 -1]
-      assert_equal {} [r lrange list2 0 -1]
-    }
+#    test "Linked BRPOPLPUSH" {
+#      set rd1 [redis_deferring_client]
+#      set rd2 [redis_deferring_client]
+#
+#      r del list1 list2 list3
+#
+#      $rd1 brpoplpush list1 list2 0
+#      $rd2 brpoplpush list2 list3 0
+#
+#      r rpush list1 foo
+#
+#      assert_equal {} [r lrange list1 0 -1]
+#      assert_equal {} [r lrange list2 0 -1]
+#      assert_equal {foo} [r lrange list3 0 -1]
+#    }
+
+#    test "Circular BRPOPLPUSH" {
+#      set rd1 [redis_deferring_client]
+#      set rd2 [redis_deferring_client]
+#
+#      r del list1 list2
+#
+#      $rd1 brpoplpush list1 list2 0
+#      $rd2 brpoplpush list2 list1 0
+#
+#      r rpush list1 foo
+#
+#      assert_equal {foo} [r lrange list1 0 -1]
+#      assert_equal {} [r lrange list2 0 -1]
+#    }
 
     test "Self-referential BRPOPLPUSH" {
       set rd [redis_deferring_client]
@@ -419,23 +419,23 @@ start_server {
         $watching_client read
     } {}
 
-    test "BRPOPLPUSH does not affect WATCH while still blocked" {
-        set blocked_client [redis_deferring_client]
-        set watching_client [redis_deferring_client]
-        r del srclist dstlist somekey
-        r set somekey somevalue
-        $blocked_client brpoplpush srclist dstlist 0
-        $watching_client watch dstlist
-        $watching_client read
-        $watching_client multi
-        $watching_client read
-        $watching_client get somekey
-        $watching_client read
-        $watching_client exec
-        # Blocked BLPOPLPUSH may create problems, unblock it.
-        r lpush srclist element
-        $watching_client read
-    } {somevalue}
+#    test "BRPOPLPUSH does not affect WATCH while still blocked" {
+#        set blocked_client [redis_deferring_client]
+#        set watching_client [redis_deferring_client]
+#        r del srclist dstlist somekey
+#        r set somekey somevalue
+#        $blocked_client brpoplpush srclist dstlist 0
+#        $watching_client watch dstlist
+#        $watching_client read
+#        $watching_client multi
+#        $watching_client read
+#        $watching_client get somekey
+#        $watching_client read
+#        $watching_client exec
+#        # Blocked BLPOPLPUSH may create problems, unblock it.
+#        r lpush srclist element
+#        $watching_client read
+#    } {somevalue}
 
     test {BRPOPLPUSH timeout} {
       set rd [redis_deferring_client]
diff --git a/native_tests/linux/tests/unit/type/set.tcl b/native_tests/linux/tests/unit/type/set.tcl
index 5df1af2..eb7ff66 100644
--- a/native_tests/linux/tests/unit/type/set.tcl
+++ b/native_tests/linux/tests/unit/type/set.tcl
@@ -1,3 +1,6 @@
+# Currently DEBUG commands such as DEBUG ENCODING are not supported in this mock
+# so those assertions are commented until it would be implemented
+
 start_server {
     tags {"set"}
     overrides {
@@ -156,12 +159,12 @@ start_server {
             assert_equal [list 195 196 197 198 199 $large] [lsort [r smembers setres]]
         }
 
-        test "SINTERSTORE with two sets, after a DEBUG RELOAD - $type" {
-            r debug reload
-            r sinterstore setres set1 set2
-            assert_encoding $type setres
-            assert_equal [list 195 196 197 198 199 $large] [lsort [r smembers setres]]
-        }
+        # test "SINTERSTORE with two sets, after a DEBUG RELOAD - $type" {
+        #     r debug reload
+        #     r sinterstore setres set1 set2
+        #     assert_encoding $type setres
+        #     assert_equal [list 195 196 197 198 199 $large] [lsort [r smembers setres]]
+        # }
 
         test "SUNION with two sets - $type" {
             set expected [lsort -uniq "[r smembers set1] [r smembers set2]"]
diff --git a/native_tests/testnative b/native_tests/testnative
new file mode 100644
index 0000000..a74ab3d
--- /dev/null
+++ b/native_tests/testnative
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+set -x
+cd linux || exit
+
+java -jar ../jm-test-server/target/jm-test-server-1.0-SNAPSHOT-jar-with-dependencies.jar &
+PID=$!
+
+wait4x redis redis://127.0.0.1:39807
+
+success=true
+for param in "$@"; do
+  ./runtest --host 127.0.0.1 --port 39807 --single "$param"
+  if [[ $? -ne 0 ]]; then
+    success=false
+    break
+  fi
+done
+
+kill $PID
+
+if $success; then
+  exit 0
+else
+  exit 1
+fi
diff --git a/src/test/java/com/github/fppt/jedismock/TestCommandParser.java b/src/test/java/com/github/fppt/jedismock/TestCommandParser.java
index 73c4b57..1e99752 100644
--- a/src/test/java/com/github/fppt/jedismock/TestCommandParser.java
+++ b/src/test/java/com/github/fppt/jedismock/TestCommandParser.java
@@ -55,7 +55,7 @@ public class TestCommandParser {
         try {
             SliceParser.consumeCount(stream);
             fail();
-        } catch (ParseErrorException e) {
+        } catch (EOFException e) {
             // OK
         }
     }
@@ -140,7 +140,7 @@ public class TestCommandParser {
         try {
             SliceParser.consumeLong(stream);
             fail();
-        } catch (ParseErrorException e) {
+        } catch (EOFException e) {
             // OK
         }
     }
@@ -151,7 +151,7 @@ public class TestCommandParser {
         try {
             SliceParser.consumeSlice(stream, 4);
             fail();
-        } catch (ParseErrorException e) {
+        } catch (EOFException e) {
             // OK
         }
     }
@@ -206,7 +206,7 @@ public class TestCommandParser {
         try {
             SliceParser.consumeParameter(stream);
             fail();
-        } catch (ParseErrorException e) {
+        } catch (EOFException e) {
             // OK
         }
     }
@@ -217,7 +217,7 @@ public class TestCommandParser {
         try {
             SliceParser.consumeParameter(stream);
             fail();
-        } catch (ParseErrorException e) {
+        } catch (EOFException e) {
             // OK
         }
     }
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/ComparisonBase.java b/src/test/java/com/github/fppt/jedismock/comparisontests/ComparisonBase.java
index bada11a..25fb5cc 100644
--- a/src/test/java/com/github/fppt/jedismock/comparisontests/ComparisonBase.java
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/ComparisonBase.java
@@ -1,7 +1,16 @@
 package com.github.fppt.jedismock.comparisontests;
 
 import com.github.fppt.jedismock.RedisServer;
-import org.junit.jupiter.api.extension.*;
+import org.junit.jupiter.api.extension.AfterAllCallback;
+import org.junit.jupiter.api.extension.AfterEachCallback;
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.Extension;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.junit.jupiter.api.extension.ParameterResolutionException;
+import org.junit.jupiter.api.extension.ParameterResolver;
+import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
+import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider;
 import org.testcontainers.containers.GenericContainer;
 import redis.clients.jedis.HostAndPort;
 import redis.clients.jedis.Jedis;
@@ -25,7 +34,7 @@ public class ComparisonBase implements TestTemplateInvocationContextProvider,
         redis.start();
 
         //Start up the fake redis server
-        fakeServer = RedisServer.newRedisServer(redis.getFirstMappedPort() + 1);
+        fakeServer = RedisServer.newRedisServer();
         fakeServer.start();
     }
 
@@ -51,8 +60,8 @@ public class ComparisonBase implements TestTemplateInvocationContextProvider,
                         new Jedis(fakeServer.getHost(), fakeServer.getBindPort(), 1000000),
                         new HostAndPort(fakeServer.getHost(), fakeServer.getBindPort())),
                 new JedisTestTemplateInvocationContext("real",
-                        new Jedis(redis.getContainerIpAddress(), redis.getFirstMappedPort()),
-                        new HostAndPort(redis.getContainerIpAddress(), redis.getFirstMappedPort())));
+                        new Jedis(redis.getHost(), redis.getFirstMappedPort()),
+                        new HostAndPort(redis.getHost(), redis.getFirstMappedPort())));
     }
 
     private static class JedisTestTemplateInvocationContext implements TestTemplateInvocationContext {
@@ -87,6 +96,12 @@ public class ComparisonBase implements TestTemplateInvocationContextProvider,
                 }
             }, (AfterEachCallback) context ->
             {
+                if (context.getExecutionException().isPresent() &&
+                        context.getExecutionException().get().getMessage().startsWith(TestErrorMessages.DEADLOCK_ERROR_MESSAGE)) {
+                    jedis.quit();
+                    jedis.close();
+                    return;
+                }
                 jedis.resetState();
                 jedis.quit();
                 jedis.close();
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/TestErrorMessages.java b/src/test/java/com/github/fppt/jedismock/comparisontests/TestErrorMessages.java
new file mode 100644
index 0000000..a406bcc
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/TestErrorMessages.java
@@ -0,0 +1,5 @@
+package com.github.fppt.jedismock.comparisontests;
+
+public class TestErrorMessages {
+    public static final String DEADLOCK_ERROR_MESSAGE = "Redis is deadlocked";
+}
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/hashes/HashOperationsTest.java b/src/test/java/com/github/fppt/jedismock/comparisontests/hashes/HashOperationsTest.java
index e87a8c8..5afe66c 100644
--- a/src/test/java/com/github/fppt/jedismock/comparisontests/hashes/HashOperationsTest.java
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/hashes/HashOperationsTest.java
@@ -7,6 +7,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
 import redis.clients.jedis.Jedis;
 import redis.clients.jedis.exceptions.JedisDataException;
 
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -299,4 +300,15 @@ public class HashOperationsTest {
         assertEquals("", jedis.hget("foo", "bar"));
     }
 
+    @TestTemplate
+    public void testHsetDoesntFailOnLongPayload(Jedis jedis) {
+        // 1001 symbol string
+        char[] buf = new char[1001];
+        Arrays.fill(buf, 'a');
+        String value = new String(buf);
+        jedis.hset("foo", "bar", value);
+        assertEquals(value, jedis.hget("foo", "bar"));
+    }
+
+
 }
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/keys/KeysOperationsTest.java b/src/test/java/com/github/fppt/jedismock/comparisontests/keys/KeysOperationsTest.java
index 1dc893a..b666cda 100644
--- a/src/test/java/com/github/fppt/jedismock/comparisontests/keys/KeysOperationsTest.java
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/keys/KeysOperationsTest.java
@@ -116,4 +116,44 @@ public class KeysOperationsTest {
         assertEquals(2, results.size());
         assertTrue(results.contains("{hashslot}:one") && results.contains("{hashslot}:two"));
     }
+
+    @TestTemplate
+    public void setNotExistsAfterAllElementsRemoved(Jedis jedis) {
+        jedis.sadd("foo", "bar");
+        jedis.srem("foo", "bar");
+        assertFalse(jedis.exists("foo"));
+        assertEquals(-2, jedis.ttl("foo"));
+    }
+
+    @TestTemplate
+    public void zSetNotExistsAfterAllElementsRemoved(Jedis jedis) {
+        jedis.zadd("foo", 42, "bar");
+        jedis.zrem("foo", "bar");
+        assertFalse(jedis.exists("foo"));
+        assertEquals(-2, jedis.ttl("foo"));
+    }
+
+    @TestTemplate
+    public void zSetNotExistsAfterAllElementsRemovedByScore(Jedis jedis) {
+        jedis.zadd("foo", 42, "bar");
+        jedis.zremrangeByScore("foo", 41, 43);
+        assertFalse(jedis.exists("foo"));
+        assertEquals(-2, jedis.ttl("foo"));
+    }
+
+    @TestTemplate
+    public void listNotExistsAfterAllElementsRemoved(Jedis jedis) {
+        jedis.lpush("foo", "bar");
+        jedis.lpop("foo");
+        assertFalse(jedis.exists("foo"));
+        assertEquals(-2, jedis.ttl("foo"));
+    }
+
+    @TestTemplate
+    public void hsetNotExistsAfterAllElementsRemoved(Jedis jedis) {
+        jedis.hset("foo", "bar", "baz");
+        jedis.hdel("foo", "bar");
+        assertFalse(jedis.exists("foo"));
+        assertEquals(-2, jedis.ttl("foo"));
+    }
 }
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/lists/AdvanceListOperationsTest.java b/src/test/java/com/github/fppt/jedismock/comparisontests/lists/AdvanceListOperationsTest.java
deleted file mode 100644
index 957a54f..0000000
--- a/src/test/java/com/github/fppt/jedismock/comparisontests/lists/AdvanceListOperationsTest.java
+++ /dev/null
@@ -1,158 +0,0 @@
-package com.github.fppt.jedismock.comparisontests.lists;
-
-import com.github.fppt.jedismock.comparisontests.ComparisonBase;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.TestTemplate;
-import org.junit.jupiter.api.extension.ExtendWith;
-import redis.clients.jedis.HostAndPort;
-import redis.clients.jedis.Jedis;
-
-import java.util.List;
-import java.util.concurrent.*;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
-
-@ExtendWith(ComparisonBase.class)
-public class AdvanceListOperationsTest {
-
-    @BeforeEach
-    public void setUp(Jedis jedis) {
-        jedis.flushAll();
-    }
-
-    @TestTemplate
-    public void whenUsingBrpoplpush_EnsureItBlocksAndCorrectResultsAreReturned(Jedis jedis, HostAndPort hostAndPort) throws ExecutionException, InterruptedException {
-        String list1key = "source list";
-        String list2key = "target list";
-
-        jedis.rpush(list2key, "a", "b", "c");
-
-        //Block on performing the BRPOPLPUSH
-        Jedis blockedClient = new Jedis(hostAndPort.getHost(), hostAndPort.getPort());
-        ExecutorService blockingThread = Executors.newSingleThreadExecutor();
-        Future future = blockingThread.submit(() -> {
-            String result = blockedClient.brpoplpush(list1key, list2key, 500);
-            assertEquals("3", result);
-        });
-
-        //Check the list is not modified
-        List<String> results = jedis.lrange(list2key, 0, -1);
-        assertEquals(3, results.size());
-
-        //Push some stuff into the blocked list
-        jedis.rpush(list1key, "1", "2", "3");
-
-        future.get();
-
-        //Check the list is modified
-        results = jedis.lrange(list2key, 0, -1);
-        assertEquals(4, results.size());
-    }
-
-    @TestTemplate
-    public void whenUsingBrpoplpushAndReachingTimeout_Return(Jedis jedis) {
-        String list1key = "another source list";
-        String list2key = "another target list";
-
-        String result = jedis.brpoplpush(list1key, list2key, 1);
-
-        assertNull(result);
-    }
-
-    @TestTemplate
-    public void whenUsingBrpoplpush_EnsureClientCanStillGetOtherResponsesInTimelyManner(Jedis jedis, HostAndPort hostAndPort) {
-        String list1key = "another another source list";
-        String list2key = "another another target list";
-
-        Jedis blockedClient = new Jedis(hostAndPort.getHost(), hostAndPort.getPort());
-        ExecutorService blockingThread = Executors.newSingleThreadExecutor();
-        blockingThread.submit(() -> {
-            String result = blockedClient.brpoplpush(list1key, list2key, 500);
-            assertEquals("3", result);
-        });
-
-        //Issue random commands to make sure mock is still responsive
-        jedis.set("k1", "v1");
-        jedis.set("k2", "v2");
-        jedis.set("k3", "v3");
-        jedis.set("k4", "v4");
-        jedis.set("k5", "v5");
-
-        //Check random commands were processed
-        assertEquals("v1", jedis.get("k1"));
-        assertEquals("v2", jedis.get("k2"));
-        assertEquals("v3", jedis.get("k3"));
-        assertEquals("v4", jedis.get("k4"));
-        assertEquals("v5", jedis.get("k5"));
-    }
-
-    @TestTemplate
-    public void whenUsingBlpop_EnsureItBlocksAndCorrectResultsAreReturned(Jedis jedis, HostAndPort hostAndPort) throws ExecutionException, InterruptedException {
-        String key = "list1_kfubdjkfnv";
-        jedis.rpush(key, "d", "e", "f");
-        //Block on performing the BLPOP
-        Jedis blockedClient = new Jedis(hostAndPort.getHost(), hostAndPort.getPort());
-        ExecutorService blockingThread = Executors.newSingleThreadExecutor();
-        Future future = blockingThread.submit(() -> {
-            List<String> result = blockedClient.blpop(10, key);
-            assertEquals(2, result.size());
-            assertEquals(key, result.get(0));
-            assertEquals("d", result.get(1));
-        });
-        future.get();
-        //Check the list is modified
-        List<String> results = jedis.lrange(key, 0, -1);
-        assertEquals(2, results.size());
-    }
-
-    @TestTemplate
-    public void whenUsingBlpop_EnsureItBlocksAndCorrectResultsAreReturnedOnMultipleList(Jedis jedis, HostAndPort hostAndPort) throws ExecutionException, InterruptedException {
-        String list1key = "list1_dkjfnvdk";
-        String list2key = "list2_kjvnddkf";
-        String list3key = "list3_oerurthv";
-
-
-        //Block on performing the BLPOP
-        Jedis blockedClient = new Jedis(hostAndPort.getHost(), hostAndPort.getPort());
-        ExecutorService blockingThread = Executors.newSingleThreadExecutor();
-        Future future = blockingThread.submit(() -> {
-            List<String> result = blockedClient.blpop(10, list1key, list2key, list3key);
-            assertEquals(list2key, result.get(0));
-            assertEquals("a", result.get(1));
-        });
-        Thread.sleep(1000);
-        jedis.rpush(list2key, "a", "b", "c");
-        jedis.rpush(list3key, "d", "e", "f");
-        future.get();
-
-        //Check the list is modified
-        List<String> results = jedis.lrange(list2key, 0, -1);
-        assertEquals(2, results.size());
-        results = jedis.lrange(list3key, 0, -1);
-        assertEquals(3, results.size());
-    }
-
-    @TestTemplate
-    public void whenUsingBlpop_EnsureItTimeout(Jedis jedis, HostAndPort hostAndPort) throws ExecutionException, InterruptedException, TimeoutException {
-        String list1key = "list1_kdjfnvdsu";
-        String list2key = "list2_mbhkdushy";
-        String list3key = "list3_qzkmpthju";
-
-        // init redisbase
-        jedis.lrange(list2key, 0, -1);
-
-        //Block on performing the BLPOP
-        Jedis blockedClient = new Jedis(hostAndPort.getHost(), hostAndPort.getPort());
-        ExecutorService blockingThread = Executors.newSingleThreadExecutor();
-        Future future = blockingThread.submit(() -> {
-            List<String> result = blockedClient.blpop(1, list1key, list2key, list3key);
-            assertNull(result);
-        });
-        //Check the list is not modified
-        jedis.getClient().setSoTimeout(2000);
-        List<String> results = jedis.lrange(list2key, 0, -1);
-        assertEquals(0, results.size());
-        future.get(4, TimeUnit.SECONDS);
-    }
-}
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/lists/BlockingOperationsTest.java b/src/test/java/com/github/fppt/jedismock/comparisontests/lists/BlockingOperationsTest.java
new file mode 100644
index 0000000..57bf797
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/lists/BlockingOperationsTest.java
@@ -0,0 +1,276 @@
+package com.github.fppt.jedismock.comparisontests.lists;
+
+import com.github.fppt.jedismock.comparisontests.ComparisonBase;
+import com.github.fppt.jedismock.comparisontests.TestErrorMessages;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.Transaction;
+import redis.clients.jedis.exceptions.JedisDataException;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+@ExtendWith(ComparisonBase.class)
+public class BlockingOperationsTest {
+
+    private ExecutorService blockingThread;
+    private Jedis blockedClient;
+
+    @BeforeEach
+    public void setUp(Jedis jedis, HostAndPort hostAndPort) {
+        jedis.flushAll();
+        blockedClient = new Jedis(hostAndPort.getHost(), hostAndPort.getPort());
+        blockingThread = Executors.newSingleThreadExecutor();
+    }
+
+    @AfterEach
+    public void tearDown() {
+        blockedClient.close();
+        blockingThread.shutdownNow();
+    }
+
+    @TestTemplate
+    public void whenUsingBrpoplpush_EnsureItBlocksAndCorrectResultsAreReturned(Jedis jedis) throws ExecutionException, InterruptedException {
+        String list1key = "source list";
+        String list2key = "target list";
+
+        jedis.rpush(list2key, "a", "b", "c");
+
+        //Block on performing the BRPOPLPUSH
+        Future<?> future = blockingThread.submit(() -> {
+            String result = blockedClient.brpoplpush(list1key, list2key, 500);
+            assertEquals("3", result);
+        });
+
+        //Check the list is not modified
+        List<String> results = jedis.lrange(list2key, 0, -1);
+        assertEquals(3, results.size());
+
+        //Push some stuff into the blocked list
+        jedis.rpush(list1key, "1", "2", "3");
+
+        future.get();
+
+        //Check the list is modified
+        results = jedis.lrange(list2key, 0, -1);
+        assertEquals(4, results.size());
+    }
+
+    @TestTemplate
+    public void whenUsingBrpoplpushAndReachingTimeout_Return(Jedis jedis) {
+        String list1key = "another source list";
+        String list2key = "another target list";
+
+        String result = jedis.brpoplpush(list1key, list2key, 1);
+
+        assertNull(result);
+    }
+
+    @TestTemplate
+    public void whenUsingBrpoplpush_EnsureClientCanStillGetOtherResponsesInTimelyManner(Jedis jedis) {
+        String list1key = "another another source list";
+        String list2key = "another another target list";
+
+        blockingThread.submit(() -> {
+            String result = blockedClient.brpoplpush(list1key, list2key, 500);
+            assertEquals("3", result);
+        });
+
+        //Issue random commands to make sure mock is still responsive
+        jedis.set("k1", "v1");
+        jedis.set("k2", "v2");
+        jedis.set("k3", "v3");
+        jedis.set("k4", "v4");
+        jedis.set("k5", "v5");
+
+        //Check random commands were processed
+        assertEquals("v1", jedis.get("k1"));
+        assertEquals("v2", jedis.get("k2"));
+        assertEquals("v3", jedis.get("k3"));
+        assertEquals("v4", jedis.get("k4"));
+        assertEquals("v5", jedis.get("k5"));
+    }
+
+    @TestTemplate
+    public void whenUsingBlpop_EnsureItBlocksAndCorrectResultsAreReturned(Jedis jedis) throws ExecutionException, InterruptedException {
+        String key = "list1_kfubdjkfnv";
+        jedis.rpush(key, "d", "e", "f");
+        //Block on performing the BLPOP
+        Future<?> future = blockingThread.submit(() -> {
+            List<String> result = blockedClient.blpop(10, key);
+            assertEquals(2, result.size());
+            assertEquals(key, result.get(0));
+            assertEquals("d", result.get(1));
+        });
+        future.get();
+        //Check the list is modified
+        List<String> results = jedis.lrange(key, 0, -1);
+        assertEquals(2, results.size());
+    }
+
+    @TestTemplate
+    public void whenUsingBlpop_EnsureItBlocksAndCorrectResultsAreReturnedOnMultipleList(Jedis jedis) throws ExecutionException, InterruptedException {
+        String list1key = "list1_dkjfnvdk";
+        String list2key = "list2_kjvnddkf";
+        String list3key = "list3_oerurthv";
+
+
+        //Block on performing the BLPOP
+        Future<?> future = blockingThread.submit(() -> {
+            List<String> result = blockedClient.blpop(10, list1key, list2key, list3key);
+            assertEquals(list2key, result.get(0));
+            assertEquals("a", result.get(1));
+        });
+        Thread.sleep(1000);
+        jedis.rpush(list2key, "a", "b", "c");
+        jedis.rpush(list3key, "d", "e", "f");
+        future.get();
+
+        //Check the list is modified
+        List<String> results = jedis.lrange(list2key, 0, -1);
+        assertEquals(2, results.size());
+        results = jedis.lrange(list3key, 0, -1);
+        assertEquals(3, results.size());
+    }
+
+    @TestTemplate
+    public void whenUsingBlpop_EnsureItTimeout(Jedis jedis) throws ExecutionException, InterruptedException, TimeoutException {
+        String list1key = "list1_kdjfnvdsu";
+        String list2key = "list2_mbhkdushy";
+        String list3key = "list3_qzkmpthju";
+
+        // init redisbase
+        jedis.lrange(list2key, 0, -1);
+
+        //Block on performing the BLPOP
+        Future<?> future = blockingThread.submit(() -> {
+            List<String> result = blockedClient.blpop(1, list1key, list2key, list3key);
+            assertNull(result);
+        });
+        //Check the list is not modified
+        jedis.getClient().setSoTimeout(2000);
+        List<String> results = jedis.lrange(list2key, 0, -1);
+        assertEquals(0, results.size());
+        future.get(4, TimeUnit.SECONDS);
+    }
+
+    @TestTemplate
+    public void whenUsingBlpop_EnsureNotWokenByTransaction(Jedis jedis) throws ExecutionException, InterruptedException, TimeoutException {
+        String listKey = "list_key";
+
+        Future<?> future = blockingThread.submit(() -> {
+            List<String> result = blockedClient.blpop(0, listKey);
+            assertNotNull(result);
+            assertEquals(2, result.size());
+            assertEquals(listKey, result.get(0));
+            assertEquals("b", result.get(1));
+        });
+
+        Transaction t = jedis.multi();
+        t.lpush(listKey, "0");
+        t.del(listKey);
+        t.exec();
+        jedis.del(listKey);
+        jedis.lpush(listKey, "b");
+        future.get(5, TimeUnit.SECONDS);
+    }
+
+    @TestTemplate
+    public void whenUsingBlpop_EnsureDoesntBlockInsideTransaction(Jedis jedis) {
+        String key = "blpop_transaction_key";
+
+        Assertions.assertTimeoutPreemptively(Duration.ofSeconds(5), () -> {
+            jedis.del(key);
+            jedis.lpush(key, "foo");
+            jedis.lpush(key, "bar");
+
+            Transaction t = jedis.multi();
+
+            t.blpop(0, key);
+            t.blpop(0, key);
+            t.blpop(0, key);
+
+            List<Object> result = t.exec();
+
+            assertEquals(
+                    Arrays.asList(
+                            Arrays.asList(key, "bar"),
+                            Arrays.asList(key, "foo"),
+                            null
+                    ), result);
+
+        }, TestErrorMessages.DEADLOCK_ERROR_MESSAGE);
+    }
+
+    @TestTemplate
+    public void whenUsingBlpop_EnsureThrowsErrorOnNegativeTimeout(Jedis jedis) {
+        String key = "blpop_negative_timeout_key";
+
+        JedisDataException exception = Assertions.assertThrows(JedisDataException.class, () -> jedis.blpop(-5, key));
+        assertEquals("ERR timeout is negative", exception.getMessage());
+
+        exception = Assertions.assertThrows(JedisDataException.class, () -> jedis.blpop(-0.1, key));
+        assertEquals("ERR timeout is negative", exception.getMessage());
+    }
+
+    @TestTemplate
+    public void whenUsingBlpop_EnsureStillWaitsIfKeyIsNotList(Jedis jedis) throws ExecutionException, InterruptedException, TimeoutException {
+        String key = "blpop_not_a_list_key";
+
+
+        Future<?> future = blockingThread.submit(() -> {
+            List<String> result = blockedClient.blpop(0, key);
+            assertEquals(2, result.size());
+            assertEquals(key, result.get(0));
+            assertEquals("foo", result.get(1));
+        });
+
+        Thread.sleep(300); // wait for blpop to execute
+
+        Transaction t = jedis.multi();
+
+        t.rpush(key, "bar");
+        t.del(key);
+        t.set(key, "bar2");
+        t.exec();
+
+        jedis.del(key);
+        jedis.lpush(key, "foo");
+
+        future.get(5, TimeUnit.SECONDS);
+    }
+
+    @TestTemplate
+    public void whenUsingBRPopLPush_ensureBlocksIndefinitely(Jedis jedis) throws InterruptedException, ExecutionException {
+        String fromKey = "brpoplpush_from";
+        String toKey = "brpoplpush_to";
+
+
+        Future<?> future = blockingThread.submit(() -> {
+            String value = blockedClient.brpoplpush(fromKey, toKey, 0);
+            assertEquals("bar", value);
+        });
+
+        // wait to be sure we wait more than 0 seconds
+        Thread.sleep(1000);
+        jedis.rpush(fromKey, "bar");
+
+        future.get();
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/lists/LInsertTest.java b/src/test/java/com/github/fppt/jedismock/comparisontests/lists/LInsertTest.java
new file mode 100644
index 0000000..0688ee9
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/lists/LInsertTest.java
@@ -0,0 +1,66 @@
+package com.github.fppt.jedismock.comparisontests.lists;
+
+import com.github.fppt.jedismock.comparisontests.ComparisonBase;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.args.ListPosition;
+
+import java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@ExtendWith(ComparisonBase.class)
+public class LInsertTest {
+
+    private final static String key = "linsert_key";
+    private final static String nonExistingkey = "linsert_key2";
+
+    @BeforeEach
+    public void setUp(Jedis jedis) {
+        jedis.flushAll();
+        jedis.rpush(key, "1", "2", "3", "3", "4");
+    }
+
+    @TestTemplate
+    @DisplayName("Check basic linsert case")
+    public void whenUsingLInsert_EnsureCorrectlyInserted(Jedis jedis) {
+        assertEquals(jedis.linsert(key, ListPosition.AFTER, "1", "10"), 6);
+        assertEquals(jedis.lrange(key, 0, -1), Arrays.asList("1", "10", "2", "3", "3", "4"));
+    }
+
+    @TestTemplate
+    @DisplayName("Check insert before first")
+    public void whenUsingLInsert_EnsureCorrectlyInsertedBeforeFirst(Jedis jedis) {
+        assertEquals(jedis.linsert(key, ListPosition.BEFORE, "1", "10"), 6);
+        assertEquals(jedis.lrange(key, 0, -1), Arrays.asList("10", "1", "2", "3", "3", "4"));
+    }
+
+    @TestTemplate
+    @DisplayName("Check insert after last")
+    public void whenUsingLInsert_EnsureCorrectlyInsertedAfterLast(Jedis jedis) {
+        assertEquals(jedis.linsert(key, ListPosition.AFTER, "4", "10"), 6);
+        assertEquals(jedis.lrange(key, 0, -1), Arrays.asList("1", "2", "3", "3", "4", "10"));
+    }
+
+    @TestTemplate
+    @DisplayName("Check choosing leftmost pivot")
+    public void whenUsingLInsert_EnsureCorrectlyChosenLeftmostPivot(Jedis jedis) {
+        assertEquals(jedis.linsert(key, ListPosition.AFTER, "3", "10"), 6);
+        assertEquals(jedis.lrange(key, 0, -1), Arrays.asList("1", "2", "3", "10", "3", "4"));
+    }
+
+    @TestTemplate
+    @DisplayName("Check non existing key")
+    public void whenUsingLInsert_EnsureReturnsZeroOnNonExistingKey(Jedis jedis) {
+        assertEquals(jedis.linsert(nonExistingkey, ListPosition.AFTER, "1", "1"), 0);
+    }
+
+    @TestTemplate
+    @DisplayName("Check for no pivot")
+    public void whenUsingLInsert_EnsureReturnsNegOneOnPivotNotFound(Jedis jedis) {
+        assertEquals(jedis.linsert(key, ListPosition.AFTER, "5", "10"), -1);
+    }
+}
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/lists/LPosTest.java b/src/test/java/com/github/fppt/jedismock/comparisontests/lists/LPosTest.java
new file mode 100644
index 0000000..c621a8f
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/lists/LPosTest.java
@@ -0,0 +1,95 @@
+package com.github.fppt.jedismock.comparisontests.lists;
+
+import com.github.fppt.jedismock.comparisontests.ComparisonBase;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.exceptions.JedisDataException;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static redis.clients.jedis.params.LPosParams.lPosParams;
+
+@ExtendWith(ComparisonBase.class)
+public class LPosTest {
+
+    private static final String key = "lpos_key";
+
+    @BeforeEach
+    public void setUp(Jedis jedis) {
+        jedis.flushAll();
+
+        jedis.rpush(key, "a", "b", "c", "1", "2", "3", "c", "c");
+    }
+
+    @TestTemplate
+    @DisplayName("Check for basic usage")
+    public void whenUsingLPos_EnsureReturnsLeftmostEntry(Jedis jedis) {
+        assertEquals(2, jedis.lpos(key, "c"));
+        assertEquals(5, jedis.lpos(key, "3"));
+        assertEquals(0, jedis.lpos(key, "a"));
+        assertNull(jedis.lpos(key, "d"));
+    }
+
+    @TestTemplate
+    @DisplayName("Check for rank param")
+    public void whenUsingLPos_EnsureRankWorksCorrectly(Jedis jedis) {
+        assertEquals(2, jedis.lpos(key, "c", lPosParams().rank(1)));
+        assertEquals(6, jedis.lpos(key, "c", lPosParams().rank(2)));
+        assertEquals(7, jedis.lpos(key, "c", lPosParams().rank(-1)));
+        assertEquals(6, jedis.lpos(key, "c", lPosParams().rank(-2)));
+        assertNull(jedis.lpos(key, "c", lPosParams().rank(4)));
+        assertNull(jedis.lpos(key, "c", lPosParams().rank(-4)));
+
+        assertThrows(JedisDataException.class, () -> jedis.lpos(key, "c", lPosParams().rank(0)));
+    }
+
+    @TestTemplate
+    @DisplayName("Check for count param")
+    public void whenUsingLPos_EnsureCountWorksCorrectly(Jedis jedis) {
+        assertEquals(singletonList(2L), jedis.lpos(key, "c", lPosParams(), 1));
+        assertEquals(asList(2L, 6L), jedis.lpos(key, "c", lPosParams(), 2));
+        assertEquals(asList(2L, 6L, 7L), jedis.lpos(key, "c", lPosParams(), 3));
+        assertEquals(asList(2L, 6L, 7L), jedis.lpos(key, "c", lPosParams(), 0));
+    }
+
+    @TestTemplate
+    @DisplayName("Check for maxlen param")
+    public void whenUsingLPos_EnsureMaxlenWorksCorrectly(Jedis jedis) {
+        assertNull(jedis.lpos(key, "1", lPosParams().maxlen(3)));
+        assertEquals(3L, jedis.lpos(key, "1", lPosParams().maxlen(4)));
+        assertEquals(3L, jedis.lpos(key, "1", lPosParams().maxlen(0)));
+    }
+
+    @TestTemplate
+    @DisplayName("Check for count and rank params combined")
+    public void whenUsingLPos_EnsureCountAndRankWorkCorrectly(Jedis jedis) {
+        assertEquals(asList(6L, 7L), jedis.lpos(key, "c", lPosParams().rank(2), 2));
+        assertEquals(asList(7L, 6L), jedis.lpos(key, "c", lPosParams().rank(-1), 2));
+        assertEquals(asList(7L, 6L, 2L), jedis.lpos(key, "c", lPosParams().rank(-1), 0));
+    }
+
+    @TestTemplate
+    @DisplayName("Check for rank and maxlen params cobined")
+    public void whenUsingLPos_EnsureRankAndMaxlenWorkCorrectly(Jedis jedis) {
+        assertEquals(2L, jedis.lpos(key, "c", lPosParams().rank(1).maxlen(3)));
+        assertNull(jedis.lpos(key, "c", lPosParams().rank(1).maxlen(2)));
+        assertEquals(7L, jedis.lpos(key, "c", lPosParams().rank(-1).maxlen(1)));
+        assertNull(jedis.lpos(key, "3", lPosParams().rank(-1).maxlen(1)));
+        assertEquals(5L, jedis.lpos(key, "3", lPosParams().rank(-1).maxlen(3)));
+    }
+
+    @TestTemplate
+    @DisplayName("Check for all params combined")
+    public void whenUsingLPos_EnsureAllParamsWorkCorrectly(Jedis jedis) {
+        assertEquals(singletonList(2L), jedis.lpos(key, "c", lPosParams().rank(1).maxlen(5), 2));
+        assertEquals(asList(2L, 6L), jedis.lpos(key, "c", lPosParams().rank(1).maxlen(7), 2));
+        assertEquals(singletonList(7L), jedis.lpos(key, "c", lPosParams().rank(-1).maxlen(1), 2));
+        assertEquals(asList(7L, 6L), jedis.lpos(key, "c", lPosParams().rank(-1).maxlen(2), 2));
+    }
+}
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/lists/LPushXRPushXTest.java b/src/test/java/com/github/fppt/jedismock/comparisontests/lists/LPushXRPushXTest.java
new file mode 100644
index 0000000..e2c4caf
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/lists/LPushXRPushXTest.java
@@ -0,0 +1,47 @@
+package com.github.fppt.jedismock.comparisontests.lists;
+
+import com.github.fppt.jedismock.comparisontests.ComparisonBase;
+import org.junit.jupiter.api.Assertions;
+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 java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@ExtendWith(ComparisonBase.class)
+public class LPushXRPushXTest {
+    private final static String lpushxKey = "lpushx_test_key";
+    private final static String rpushxKey = "rpushx_test_key";
+
+    @BeforeEach
+    public void setUp(Jedis jedis) {
+        jedis.flushAll();
+    }
+
+    @TestTemplate
+    public void whenUsingLPushX_EnsureReturnsZeroOnNonList(Jedis jedis) {
+        assertEquals(0, jedis.lpushx(lpushxKey, "foo"));
+    }
+
+    @TestTemplate
+    public void whenUsingLPushX_EnsurePushesCorrectly(Jedis jedis) {
+        jedis.lpush(lpushxKey, "fooo");
+        assertEquals(3, jedis.lpushx(lpushxKey, "bar", "foo"));
+        assertEquals(Arrays.asList("foo", "bar"), jedis.lrange(lpushxKey, 0, 1));
+    }
+
+    @TestTemplate
+    public void whenUsingRPushX_EnsureReturnsZeroOnNonList(Jedis jedis) {
+        assertEquals(0, jedis.lpushx(rpushxKey, "foo"));
+    }
+
+    @TestTemplate
+    public void whenUsingRPushX_EnsurePushesCorrectly(Jedis jedis) {
+        jedis.rpush(rpushxKey, "fooo");
+        assertEquals(3, jedis.rpushx(rpushxKey, "bar", "foo"));
+        assertEquals(Arrays.asList("bar", "foo"), jedis.lrange(rpushxKey, 1, 2));
+    }
+}
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/lists/LRangeTest.java b/src/test/java/com/github/fppt/jedismock/comparisontests/lists/LRangeTest.java
new file mode 100644
index 0000000..0c4b6fb
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/lists/LRangeTest.java
@@ -0,0 +1,26 @@
+package com.github.fppt.jedismock.comparisontests.lists;
+
+import com.github.fppt.jedismock.comparisontests.ComparisonBase;
+import org.junit.jupiter.api.Assertions;
+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 java.util.Collections;
+
+@ExtendWith(ComparisonBase.class)
+public class LRangeTest {
+    @BeforeEach
+    public void setUp(Jedis jedis) {
+        jedis.flushAll();
+    }
+
+    @TestTemplate
+    public void whenUsingLRange_checkOutOfRangeNegativeEndIndex(Jedis jedis) {
+        String key = "lrange_key";
+        jedis.lpush(key, "1", "2", "3");
+
+        Assertions.assertEquals(Collections.emptyList(), jedis.lrange(key, 0, -5));
+    }
+}
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/lists/LSetTest.java b/src/test/java/com/github/fppt/jedismock/comparisontests/lists/LSetTest.java
new file mode 100644
index 0000000..d8031eb
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/lists/LSetTest.java
@@ -0,0 +1,51 @@
+package com.github.fppt.jedismock.comparisontests.lists;
+
+import com.github.fppt.jedismock.comparisontests.ComparisonBase;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.exceptions.JedisDataException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+@ExtendWith(ComparisonBase.class)
+public class LSetTest {
+    private static final String key = "lset_list_key";
+    @BeforeEach
+    public void setUp(Jedis jedis) {
+        jedis.flushAll();
+        jedis.rpush(key, "1", "2", "3");
+    }
+
+    @TestTemplate
+    @DisplayName("Change element")
+    public void whenUsingLSet_EnsureElementIsSet(Jedis jedis) {
+        jedis.lset(key, 1, "5");
+        assertEquals(jedis.lindex(key, 1), "5");
+    }
+
+    @TestTemplate
+    @DisplayName("Change element by negative index")
+    public void whenUsingLSet_EnsureNegativeIndexWorks(Jedis jedis) {
+        jedis.lset(key, -2, "5");
+        assertEquals(jedis.lindex(key, 1), "5");
+    }
+
+    @TestTemplate
+    @DisplayName("Check index out of bound returns error")
+    public void whenUsingLSet_EnsureErrorOnIndexOutOfBounds(Jedis jedis) {
+        assertThrows(JedisDataException.class, () -> jedis.lset(key, 10, "5"), "ERR index out of range");
+        assertThrows(JedisDataException.class, () -> jedis.lset(key, -10, "5"), "ERR index out of range");
+    }
+
+    @TestTemplate
+    @DisplayName("Check error on non existing key")
+    public void whenUsingLSet_EnsureErrorOnNonExistingKey(Jedis jedis) {
+        jedis.del(key);
+        JedisDataException exception = assertThrows(JedisDataException.class, () -> jedis.lset(key, 0, "1"));
+        assertEquals(exception.getMessage(), "ERR no such key");
+    }
+}
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/lists/ListPopTest.java b/src/test/java/com/github/fppt/jedismock/comparisontests/lists/ListPopTest.java
new file mode 100644
index 0000000..1bef40c
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/lists/ListPopTest.java
@@ -0,0 +1,30 @@
+package com.github.fppt.jedismock.comparisontests.lists;
+
+import com.github.fppt.jedismock.comparisontests.ComparisonBase;
+import org.junit.jupiter.api.Assertions;
+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.exceptions.JedisDataException;
+
+@ExtendWith(ComparisonBase.class)
+public class ListPopTest {
+
+    @BeforeEach
+    public void setUp(Jedis jedis) {
+        jedis.flushAll();
+    }
+
+    @TestTemplate
+    public void whenUsingListPop_ensureCheckForType(Jedis jedis) {
+        String key = "pop_key";
+        jedis.set(key, "notalist");
+
+        JedisDataException exception = Assertions.assertThrows(JedisDataException.class, () -> jedis.rpop(key));
+        Assertions.assertTrue(exception.getMessage().startsWith("WRONGTYPE"));
+
+        exception = Assertions.assertThrows(JedisDataException.class, () -> jedis.lpop(key));
+        Assertions.assertTrue(exception.getMessage().startsWith("WRONGTYPE"));
+    }
+}
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/lists/RPopLPushTest.java b/src/test/java/com/github/fppt/jedismock/comparisontests/lists/RPopLPushTest.java
new file mode 100644
index 0000000..136c195
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/lists/RPopLPushTest.java
@@ -0,0 +1,32 @@
+package com.github.fppt.jedismock.comparisontests.lists;
+
+import com.github.fppt.jedismock.comparisontests.ComparisonBase;
+import org.junit.jupiter.api.Assertions;
+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.exceptions.JedisDataException;
+
+@ExtendWith(ComparisonBase.class)
+public class RPopLPushTest {
+
+    @BeforeEach
+    public void setUp(Jedis jedis) {
+        jedis.flushAll();
+    }
+
+    @TestTemplate
+    public void whenUsingRRopLPush_ensureDontPopOnInvalidTargetType(Jedis jedis) {
+        String listKey = "rpoplpush_1_list";
+        String valueKey = "rpoplpush_1_value";
+        jedis.rpush(listKey, "1", "2", "3");
+        jedis.set(valueKey, "some_value");
+
+        JedisDataException e = Assertions.assertThrows(JedisDataException.class, () -> jedis.rpoplpush(listKey, valueKey));
+
+        Assertions.assertEquals("WRONGTYPE Operation against a key holding the wrong kind of value", e.getMessage());
+
+        Assertions.assertEquals(3, jedis.llen(listKey));
+    }
+}
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/lists/SortTest.java b/src/test/java/com/github/fppt/jedismock/comparisontests/lists/SortTest.java
new file mode 100644
index 0000000..7a32e3b
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/lists/SortTest.java
@@ -0,0 +1,98 @@
+package com.github.fppt.jedismock.comparisontests.lists;
+
+import com.github.fppt.jedismock.comparisontests.ComparisonBase;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.Timeout;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.testcontainers.shaded.com.google.common.collect.Lists;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.args.SortingOrder;
+import redis.clients.jedis.exceptions.JedisDataException;
+import redis.clients.jedis.params.SortingParams;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+@ExtendWith(ComparisonBase.class)
+public class SortTest {
+
+    private static final String key = "sort_key";
+    private static final String numerical_sort_key = "numerical_sort_key";
+    private static final String store_sort_key = "store_sort_key";
+
+    @BeforeEach
+    public void setUp(Jedis jedis) {
+        jedis.flushAll();
+
+        jedis.rpush(key, "a", "b", "c", "1", "2", "3", "c", "c");
+        jedis.rpush(numerical_sort_key, "5", "4", "3", "2", "1");
+    }
+
+    @TestTemplate
+    public void whenUsingSort_EnsureSortsNumerical(Jedis jedis) {
+        assertEquals(Arrays.asList("1", "2", "3", "4", "5"), jedis.sort(numerical_sort_key));
+        assertEquals(Arrays.asList("5", "4", "3", "2", "1"), jedis.sort(numerical_sort_key, new SortingParams().sortingOrder(SortingOrder.DESC)));
+        assertEquals(Arrays.asList("1", "2", "3", "4", "5"), jedis.sort(numerical_sort_key, new SortingParams().sortingOrder(SortingOrder.ASC)));
+    }
+
+    @TestTemplate
+    public void whenUsingSort_EnsureSortsAlphabetically(Jedis jedis) {
+        List<String> result = Arrays.asList("1", "2", "3", "a", "b", "c", "c", "c");
+        assertEquals(result, jedis.sort(key, new SortingParams().sortingOrder(SortingOrder.ASC).alpha()));
+        assertEquals(Lists.reverse(result), jedis.sort(key, new SortingParams().sortingOrder(SortingOrder.DESC).alpha()));
+    }
+
+    @TestTemplate
+    public void whenUsingSort_EnsureStores(Jedis jedis) {
+        assertEquals(5, jedis.sort(numerical_sort_key, new SortingParams().sortingOrder(SortingOrder.DESC), store_sort_key));
+        assertEquals(Arrays.asList("5", "4", "3", "2", "1"), jedis.lrange(numerical_sort_key, 0, 5));
+    }
+
+    @TestTemplate
+    public void whenUsingSort_EnsureThrowsOnInvalidType(Jedis jedis) {
+        JedisDataException e = Assertions.assertThrows(JedisDataException.class, () -> jedis.sort(key));
+
+        assertEquals(e.getMessage(), "ERR One or more scores can't be converted into double");
+    }
+
+    @TestTemplate
+    public void whenUsingSort_EnsureHandlesLimit(Jedis jedis) {
+        List<String> sortedResult = Arrays.asList("1", "2", "3", "a", "b", "c", "c", "c");
+        assertEquals(sortedResult.subList(1, 4), jedis.sort(key, new SortingParams().alpha().limit(1, 3)));
+        assertEquals(sortedResult.subList(1, 8), jedis.sort(key, new SortingParams().alpha().limit(1, 100)));
+        assertEquals(sortedResult.subList(0, 4), jedis.sort(key, new SortingParams().alpha().limit(0, 4)));
+        assertEquals(sortedResult.subList(0, 4), jedis.sort(key, new SortingParams().alpha().limit(-100, 4)));
+    }
+
+    @TestTemplate
+    @Timeout(value = 3)
+    public void whenUsingSort_EnsureWakesOnStore(Jedis jedis, HostAndPort hostAndPort) throws InterruptedException, ExecutionException {
+        Jedis blockingClient = new Jedis(hostAndPort);
+        ExecutorService e = Executors.newSingleThreadExecutor();
+
+        Future<?> future = e.submit(() -> {
+            List<String> result = blockingClient.blpop(0, store_sort_key);
+
+            assertEquals(2, result.size());
+            assertEquals(store_sort_key, result.get(0));
+            assertEquals("1", result.get(1));
+        });
+
+        Thread.sleep(100);
+
+        jedis.sort(numerical_sort_key, new SortingParams(), store_sort_key);
+        future.get();
+        e.shutdownNow();
+        blockingClient.close();
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/lists/WatchTest.java b/src/test/java/com/github/fppt/jedismock/comparisontests/lists/WatchTest.java
new file mode 100644
index 0000000..d1470c8
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/lists/WatchTest.java
@@ -0,0 +1,39 @@
+package com.github.fppt.jedismock.comparisontests.lists;
+
+import com.github.fppt.jedismock.comparisontests.ComparisonBase;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.Transaction;
+
+@ExtendWith(ComparisonBase.class)
+public class WatchTest {
+
+    @BeforeEach
+    public void setUp(Jedis jedis) {
+        jedis.flushAll();
+    }
+
+    @TestTemplate
+    public void whenUsingPop_ensureAffectsWatchedKey(Jedis jedis, HostAndPort hostAndPort) {
+        String key = "watch_key";
+        String setKey = "set_key";
+        String initialValue = "initial_value";
+
+        Jedis secondClient = new Jedis(hostAndPort);
+
+        jedis.set(setKey, initialValue);
+        jedis.lpush(key, "1", "2", "3");
+        jedis.watch(key);
+
+        Transaction t = jedis.multi();
+        t.set(setKey, "some_value");
+        secondClient.rpop(key);
+        t.exec();
+
+        Assertions.assertEquals(initialValue, jedis.get(setKey));
+    }
+}
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/scripting/EvalShaTest.java b/src/test/java/com/github/fppt/jedismock/comparisontests/scripting/EvalShaTest.java
new file mode 100644
index 0000000..994ff56
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/scripting/EvalShaTest.java
@@ -0,0 +1,55 @@
+package com.github.fppt.jedismock.comparisontests.scripting;
+
+import com.github.fppt.jedismock.comparisontests.ComparisonBase;
+import com.github.fppt.jedismock.operations.scripting.Script;
+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 static org.junit.jupiter.api.Assertions.*;
+
+@ExtendWith(ComparisonBase.class)
+class EvalShaTest {
+
+    @BeforeEach
+    public void setUp(Jedis jedis) {
+        jedis.flushAll();
+    }
+
+    @TestTemplate
+    public void evalShaWorksLowercase(Jedis jedis) {
+        String script =
+                "redis.call('SADD', 'foo', 'bar')\n" +
+                        "return 'Hello, scripting! (lowercase SHA)'";
+        Object evalResult = jedis.eval(script, 0);
+        String sha = Script.getScriptSHA(script).toLowerCase();
+        assertTrue(jedis.scriptExists(sha));
+        assertEquals(evalResult, jedis.evalsha(sha, 0));
+    }
+
+    @TestTemplate
+    public void evalShaWorksUppercase(Jedis jedis) {
+        String script =
+                "redis.call('SADD', 'foo', 'bar')\n" +
+                        "return 'Hello, scripting! (uppercase SHA)'";
+        Object evalResult = jedis.eval(script, 0);
+        String sha = Script.getScriptSHA(script).toUpperCase();
+        assertTrue(jedis.scriptExists(sha));
+        assertEquals(evalResult, jedis.evalsha(sha, 0));
+    }
+
+
+    @TestTemplate
+    public void evalShaWithScriptLoadingWorks(Jedis jedis) {
+        String script = "return 'Hello, ' .. ARGV[1] .. '!'";
+        String sha = jedis.scriptLoad(script);
+        assertEquals("Hello, world!", jedis.evalsha(sha, 0, "world"));
+    }
+
+    @TestTemplate
+    public void evalShaNotFoundExceptionIsCorrect(Jedis jedis) {
+        RuntimeException e = assertThrows(RuntimeException.class, () -> jedis.evalsha("abc", 0));
+        assertEquals("NOSCRIPT No matching script. Please use EVAL.", e.getMessage());
+    }
+}
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/scripting/EvalTest.java b/src/test/java/com/github/fppt/jedismock/comparisontests/scripting/EvalTest.java
new file mode 100644
index 0000000..2c82c74
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/scripting/EvalTest.java
@@ -0,0 +1,255 @@
+package com.github.fppt.jedismock.comparisontests.scripting;
+
+import com.github.fppt.jedismock.comparisontests.ComparisonBase;
+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.exceptions.JedisDataException;
+
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@ExtendWith(ComparisonBase.class)
+public class EvalTest {
+    @BeforeEach
+    void setUp(Jedis jedis) {
+        jedis.flushAll();
+    }
+
+    @TestTemplate
+    void evalTest(Jedis jedis) {
+        Object eval_return = jedis.eval("return 'Hello, scripting!'", 0);
+        assertEquals(String.class, eval_return.getClass());
+        assertEquals("Hello, scripting!", eval_return);
+    }
+
+    @TestTemplate
+    void evalParametrizedTest(Jedis jedis) {
+        Object eval_return = jedis.eval("return ARGV[1]", 0, "Hello");
+        assertEquals(String.class, eval_return.getClass());
+        assertEquals("Hello", eval_return);
+    }
+
+    @TestTemplate
+    void evalIntTest(Jedis jedis) {
+        Object eval_return = jedis.eval("return 0", 0);
+        assertEquals(Long.class, eval_return.getClass());
+        assertEquals(0L, eval_return);
+    }
+
+    @TestTemplate
+    void evalLongTest(Jedis jedis) {
+        Object eval_return = jedis.eval("return 1.123", 0);
+        assertEquals(Long.class, eval_return.getClass());
+        assertEquals(1L, eval_return);
+    }
+
+    @TestTemplate
+    void evalTableOfStringsTest(Jedis jedis) {
+        Object eval_return = jedis.eval("return { 'test' }", 0);
+        assertEquals(ArrayList.class, eval_return.getClass());
+        assertEquals(Collections.singletonList("test"), eval_return);
+    }
+
+    @TestTemplate
+    void evalTableOfLongTest(Jedis jedis) {
+        Object eval_return = jedis.eval("return { 1, 2, 3 }", 0);
+        assertEquals(ArrayList.class, eval_return.getClass());
+        assertEquals(Long.class, ((List<?>) eval_return).get(0).getClass());
+        assertEquals(Arrays.asList(1L, 2L, 3L), eval_return);
+    }
+
+    @TestTemplate
+    void evalDeepListTest(Jedis jedis) {
+        Object eval_return = jedis.eval("return { 'test', 2, {'test', 2} }", 0);
+        assertEquals(ArrayList.class, eval_return.getClass());
+        assertEquals(String.class, ((List<?>) eval_return).get(0).getClass());
+        assertEquals(Long.class, ((List<?>) eval_return).get(1).getClass());
+        assertEquals(ArrayList.class, ((List<?>) eval_return).get(2).getClass());
+        assertEquals(Arrays.asList("test", 2L, Arrays.asList("test", 2L)), eval_return);
+    }
+
+    @TestTemplate
+    void evalDictTest(Jedis jedis) {
+        Object eval_return = jedis.eval("return { a = 1, 2 }", 0);
+        assertEquals(ArrayList.class, eval_return.getClass());
+        assertEquals(Long.class, ((List<?>) eval_return).get(0).getClass());
+        assertEquals(Collections.singletonList(2L), eval_return);
+    }
+
+    @TestTemplate
+    void okFieldConversion(Jedis jedis) {
+        String script = "return {ok='fine'}";
+        assertEquals("fine", jedis.eval(script, 0));
+    }
+
+    @TestTemplate
+    void errFieldConversion(Jedis jedis) {
+        String script = "return {err='bad'}";
+        String message = assertThrows(JedisDataException.class,
+                () -> jedis.eval(script, 0)).getMessage();
+        assertEquals("bad", message);
+    }
+
+    @TestTemplate
+    void statusReplyAPI(Jedis jedis) {
+        String script = "return redis.status_reply('Everything is fine')";
+        assertEquals("Everything is fine", jedis.eval(script, 0));
+    }
+
+    @TestTemplate
+    void errorReplyAPI(Jedis jedis) {
+        String script = "return redis.error_reply('Something bad happened')";
+        String message = assertThrows(JedisDataException.class,
+                () -> jedis.eval(script, 0)).getMessage();
+        assertEquals("Something bad happened", message);
+    }
+
+    @TestTemplate
+    void logLevelsAPI(Jedis jedis) {
+        assertEquals(0L, jedis.eval("return redis.LOG_DEBUG"));
+        assertEquals(1L, jedis.eval("return redis.LOG_VERBOSE"));
+        assertEquals(2L, jedis.eval("return redis.LOG_NOTICE"));
+        assertEquals(3L, jedis.eval("return redis.LOG_WARNING"));
+    }
+
+    @TestTemplate
+    void logAPI(Jedis jedis) {
+        assertNull(jedis.eval("return redis.log(redis.LOG_DEBUG, 'Something is happening')"));
+        assertNull(jedis.eval("return redis.log(redis.LOG_VERBOSE, 'Blah-blah')"));
+        assertNull(jedis.eval("return redis.log(redis.LOG_NOTICE, 'Notice this')"));
+        assertNull(jedis.eval("return redis.log(redis.LOG_WARNING, 'Something is wrong')"));
+    }
+
+    @TestTemplate
+    void evalParametrizedReturnMultipleKeysArgsTest(Jedis jedis) {
+        Object eval_return = jedis.eval(
+                "return { KEYS[1], KEYS[2], ARGV[1], ARGV[2], ARGV[3] }",
+                2, "key1", "key2",
+                "arg1", "arg2", "arg3"
+        );
+        assertEquals(ArrayList.class, eval_return.getClass());
+        assertEquals(Arrays.asList("key1", "key2", "arg1", "arg2", "arg3"), eval_return);
+    }
+
+    @TestTemplate
+    void evalParametrizedReturnMultipleKeysArgsNumbersTest(Jedis jedis) {
+        Object eval_return = jedis.eval(
+                "return { KEYS[1], KEYS[2], tonumber(ARGV[1]) }",
+                2, "key1", "key2",
+                "1"
+        );
+        assertEquals(ArrayList.class, eval_return.getClass());
+        assertEquals(Arrays.asList("key1", "key2", 1L), eval_return);
+    }
+
+    @TestTemplate
+    void evalRedisSetTest(Jedis jedis) {
+        assertEquals("OK", jedis.eval("return redis.call('SET', 'test', 'hello')", 0));
+        assertEquals("hello", jedis.get("test"));
+    }
+
+    @TestTemplate
+    void evalRedisDecrTest(Jedis jedis) {
+        jedis.eval("redis.call('SET', 'count', '1')", 0);
+        assertEquals(0L, jedis.eval("return redis.call('DECR', 'count')", 0));
+    }
+
+    @TestTemplate
+    void evalRedisRecursiveTest(Jedis jedis) {
+        Exception e = assertThrows(RuntimeException.class, () -> jedis.eval("return redis.call('EVAL', 'return { 1, 2, 3 }', '0')", 0));
+        assertNotNull(e);
+    }
+
+    @TestTemplate
+    void evalRedisReturnPcallResultsInExceptionTest(Jedis jedis) {
+        JedisDataException e = assertThrows(JedisDataException.class, () -> jedis.eval("return redis.pcall('RENAME','A','B')", 0));
+        assertNotNull(e);
+    }
+
+    @TestTemplate
+    void evalRedisPCallCanHandleExceptionTest(Jedis jedis) {
+        assertEquals("Handled error from pcall", jedis.eval("" +
+                        "local reply = redis.pcall('RENAME','A','B')\n" +
+                        "if reply['err'] ~= nil then\n" +
+                        "  return 'Handled error from pcall'" +
+                        "end\n" +
+                        "return reply",
+                0));
+    }
+
+    @TestTemplate
+    void evalRedisPCallDoesNotThrowTest(Jedis jedis) {
+        assertNull(jedis.eval("redis.pcall('RENAME','A','B')", 0));
+    }
+
+    @TestTemplate
+    void fibonacciScript(Jedis jedis) {
+        String script =
+                "local a, b = 0, 1\n" +
+                        "for i = 2, ARGV[1] do\n" +
+                        "  local temp = a + b\n" +
+                        "  a = b\n" +
+                        "  b = temp\n" +
+                        "  redis.call('RPUSH',KEYS[1], temp)\n" +
+                        "end\n";
+        jedis.eval(script, 1, "mylist", "10");
+        assertEquals(Arrays.asList("1", "2", "3", "5", "8", "13", "21", "34", "55"),
+                jedis.lrange("mylist", 0, -1));
+    }
+
+    @TestTemplate
+    void trailingComment(Jedis jedis) {
+        assertEquals("hello", jedis.eval("return 'hello' --trailing comment", 0));
+    }
+
+    @TestTemplate
+    void manyArgumentsTest(Jedis jedis) {
+        String script = "return redis.call('SADD', 'myset', 1, 2, 3, 4, 5, 6)";
+        jedis.eval(script, 0);
+        assertEquals(6, jedis.scard("myset"));
+    }
+
+    @TestTemplate
+    void booleanTrueConversion(Jedis jedis) {
+        String script = "return true";
+        assertEquals(1L, jedis.eval(script, 0));
+    }
+
+    @TestTemplate
+    void booleanFalseConversion(Jedis jedis) {
+        String script = "return false";
+        assertNull(jedis.eval(script, 0));
+    }
+
+    @TestTemplate
+    void sha1hexImplementation(Jedis jedis) {
+        String script = "return redis.sha1hex('Pizza & Mandolino')";
+        assertEquals("74822d82031af7493c20eefa13bd07ec4fada82f",
+                jedis.eval(script, 0));
+    }
+
+    @TestTemplate
+    void selectUsesSelectedDB(Jedis jedis) {
+        jedis.select(5);
+        jedis.set("foo", "DB5");
+        jedis.select(6);
+        jedis.set("foo", "DB6");
+        jedis.select(5);
+        assertEquals("DB5", jedis.eval("return redis.call('get', 'foo')"));
+    }
+
+    @TestTemplate
+    void luaSelectDoesNotAffectSelectedDB(Jedis jedis) {
+        jedis.select(5);
+        jedis.set("foo", "DB5");
+        jedis.select(6);
+        jedis.set("foo", "DB6");
+        assertEquals("DB5", jedis.eval("redis.call('select', 5); return redis.call('get', 'foo')"));
+        assertEquals("DB6", jedis.get("foo"));
+    }
+
+
+}
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/scripting/ScriptTest.java b/src/test/java/com/github/fppt/jedismock/comparisontests/scripting/ScriptTest.java
new file mode 100644
index 0000000..501cd7b
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/scripting/ScriptTest.java
@@ -0,0 +1,48 @@
+package com.github.fppt.jedismock.comparisontests.scripting;
+
+import com.github.fppt.jedismock.comparisontests.ComparisonBase;
+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 java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@ExtendWith(ComparisonBase.class)
+class ScriptTest {
+
+    @BeforeEach
+    public void setUp(Jedis jedis) {
+        jedis.flushAll();
+    }
+
+    @TestTemplate
+    public void loadTest(Jedis jedis) {
+        String sha = jedis.scriptLoad("return 'Hello'");
+        assertEquals(sha, sha.toLowerCase());
+        assertEquals("Hello", jedis.evalsha(sha));
+        assertTrue(jedis.scriptExists(sha));
+    }
+
+    @TestTemplate
+    public void loadParametrizedTest(Jedis jedis) {
+        String sha = jedis.scriptLoad("return ARGV[1]");
+        String supposedReturn = "Hello, scripting!";
+        Object response = jedis.evalsha(sha, 0, supposedReturn);
+        assertEquals(String.class, response.getClass());
+        assertEquals(supposedReturn, response);
+        assertTrue(jedis.scriptExists(sha));
+    }
+
+    @TestTemplate
+    public void scriptFlushRemovesScripts(Jedis jedis) {
+        String s1 = jedis.scriptLoad("return 1");
+        String s2 = jedis.scriptLoad("return 2");
+        assertEquals(Arrays.asList(true, true), jedis.scriptExists(s1, s2));
+        jedis.scriptFlush();
+        assertEquals(Arrays.asList(false, false), jedis.scriptExists(s1, s2));
+    }
+}
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/sets/SDiffSDiffStoreTest.java b/src/test/java/com/github/fppt/jedismock/comparisontests/sets/SDiffSDiffStoreTest.java
new file mode 100644
index 0000000..9b823b0
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/sets/SDiffSDiffStoreTest.java
@@ -0,0 +1,97 @@
+package com.github.fppt.jedismock.comparisontests.sets;
+
+import com.github.fppt.jedismock.comparisontests.ComparisonBase;
+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 java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@ExtendWith(ComparisonBase.class)
+public class SDiffSDiffStoreTest {
+    @BeforeEach
+    public void setUp(Jedis jedis) {
+        jedis.flushAll();
+    }
+
+    @TestTemplate
+    public void sDiffTwoSetsTest(Jedis jedis) {
+        String key1 = "set1";
+        String key2 = "set2";
+        Set<String> mySet1 = new HashSet<>(Arrays.asList("a", "b", "c", "d"));
+        Set<String> mySet2 = new HashSet<>(Arrays.asList("c", "d", "e"));
+
+        Set<String> expectedDifference = new HashSet<>(Arrays.asList("a", "b"));
+
+        //Add everything from the sets
+        mySet1.forEach(value -> jedis.sadd(key1, value));
+        mySet2.forEach(value -> jedis.sadd(key2, value));
+
+
+        Set<String> result = jedis.sdiff(key1, key2);
+        assertEquals(2, result.size());
+        assertEquals(expectedDifference, result);
+    }
+
+    @TestTemplate
+    public void sDiffThreeSetsTest(Jedis jedis) {
+        String key1 = "set1";
+        String key2 = "set2";
+        String key3 = "set3";
+        Set<String> mySet1 = new HashSet<>(Arrays.asList("a", "b", "c", "d"));
+        Set<String> mySet2 = new HashSet<>(Collections.singletonList("c"));
+        Set<String> mySet3 = new HashSet<>(Arrays.asList("a", "c", "e"));
+
+        Set<String> expectedDifference = new HashSet<>(Arrays.asList("b", "d"));
+
+        //Add everything from the sets
+        mySet1.forEach(value -> jedis.sadd(key1, value));
+        mySet2.forEach(value -> jedis.sadd(key2, value));
+        mySet3.forEach(value -> jedis.sadd(key3, value));
+
+
+        Set<String> result = jedis.sdiff(key1, key2, key3);
+        assertEquals(2, result.size());
+        assertEquals(expectedDifference, result);
+    }
+
+    @TestTemplate
+    public void sDiffStoreTest(Jedis jedis) {
+        String key1 = "set1";
+        String key2 = "set2";
+        Set<String> mySet1 = new HashSet<>(Arrays.asList("a", "b", "c", "d"));
+        Set<String> mySet2 = new HashSet<>(Arrays.asList("c", "d", "e"));
+
+        Set<String> expectedDifference = new HashSet<>(Arrays.asList("a", "b"));
+
+        //Add everything from the sets
+        mySet1.forEach(value -> jedis.sadd(key1, value));
+        mySet2.forEach(value -> jedis.sadd(key2, value));
+
+        String destination = "set3";
+
+        Long elementsInDifference = jedis.sdiffstore(destination, key1, key2);
+        assertEquals(2, elementsInDifference);
+
+        assertEquals(expectedDifference, jedis.smembers(destination));
+    }
+
+
+    @TestTemplate
+    public void deletesDestinationIfResultIsEmpty(Jedis jedis) {
+        jedis.sadd("dest", "a", "b");
+        jedis.sadd("src", "c");
+        jedis.sadd("other", "c", "d");
+        assertTrue(jedis.exists("dest"));
+        jedis.sdiffstore("dest", "src", "other");
+        assertFalse(jedis.exists("dest"));
+    }
+}
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/sets/SInterSInterStoreTest.java b/src/test/java/com/github/fppt/jedismock/comparisontests/sets/SInterSInterStoreTest.java
new file mode 100644
index 0000000..ce6a06e
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/sets/SInterSInterStoreTest.java
@@ -0,0 +1,80 @@
+package com.github.fppt.jedismock.comparisontests.sets;
+
+import com.github.fppt.jedismock.comparisontests.ComparisonBase;
+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 java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@ExtendWith(ComparisonBase.class)
+public class SInterSInterStoreTest {
+    @BeforeEach
+    public void setUp(Jedis jedis) {
+        jedis.flushAll();
+    }
+
+    @TestTemplate
+    public void whenUsingHsinter_EnsureSetIntersectionIsReturned(Jedis jedis) {
+        String key1 = "my-set-key-1";
+        Set<String> mySet1 = new HashSet<>(Arrays.asList("a", "b", "c", "d"));
+        String key2 = "my-set-key-2";
+        Set<String> mySet2 = new HashSet<>(Arrays.asList("b", "d", "e", "f"));
+        String key3 = "my-set-key-3";
+        Set<String> mySet3 = new HashSet<>(Arrays.asList("b", "e", "f"));
+
+        Set<String> expectedIntersection1 = new HashSet<>(Arrays.asList("b", "d"));
+        Set<String> expectedIntersection2 = new HashSet<>(Collections.singletonList("b"));
+
+        //Add everything from the sets
+        mySet1.forEach(value -> jedis.sadd(key1, value));
+        mySet2.forEach(value -> jedis.sadd(key2, value));
+        mySet3.forEach(value -> jedis.sadd(key3, value));
+
+        Set<String> intersection = jedis.sinter(key1, key2);
+        assertEquals(expectedIntersection1, intersection);
+
+        intersection = jedis.sinter(key1, key2, key3);
+        assertEquals(expectedIntersection2, intersection);
+    }
+
+    @TestTemplate
+    public void whenUsingSInterStore_testIntersectionIsStored(Jedis jedis) {
+        String key1 = "set1";
+        String key2 = "set2";
+        Set<String> mySet1 = new HashSet<>(Arrays.asList("a", "b", "c", "d"));
+        Set<String> mySet2 = new HashSet<>(Arrays.asList("b", "d", "e", "f"));
+
+        Set<String> expectedIntersection = new HashSet<>(Arrays.asList("b", "d"));
+
+        //Add everything from the sets
+        mySet1.forEach(value -> jedis.sadd(key1, value));
+        mySet2.forEach(value -> jedis.sadd(key2, value));
+
+        String destination = "set3";
+
+        Long elementsInIntersection = jedis.sinterstore(destination, key1, key2);
+        assertEquals(2, elementsInIntersection);
+
+        assertEquals(expectedIntersection, jedis.smembers(destination));
+    }
+
+    @TestTemplate
+    public void deletesDestinationIfResultIsEmpty(Jedis jedis) {
+        jedis.sadd("dest", "a", "b");
+        jedis.sadd("src", "c", "d");
+        jedis.sadd("other", "e", "f");
+        assertTrue(jedis.exists("dest"));
+        jedis.sinterstore("dest", "src", "other");
+        assertFalse(jedis.exists("dest"));
+    }
+
+}
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/sets/SRandMemberTest.java b/src/test/java/com/github/fppt/jedismock/comparisontests/sets/SRandMemberTest.java
new file mode 100644
index 0000000..e748f1b
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/sets/SRandMemberTest.java
@@ -0,0 +1,88 @@
+package com.github.fppt.jedismock.comparisontests.sets;
+
+import com.github.fppt.jedismock.comparisontests.ComparisonBase;
+import org.junit.jupiter.api.Assertions;
+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 java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@ExtendWith(ComparisonBase.class)
+public class SRandMemberTest {
+    @BeforeEach
+    public void setUp(Jedis jedis) {
+        jedis.flushAll();
+    }
+
+    @TestTemplate
+    void randMemberReturnsARandomElementOfTheSet(Jedis jedis) {
+        Collection<String> set = Arrays.asList("a", "b", "c");
+        jedis.sadd("foo", set.toArray(new String[0]));
+        Set<String> usedElements = new HashSet<>();
+        for (int i = 0; i < 5000; i++) {
+            String member = jedis.srandmember("foo");
+            Assertions.assertTrue(set.contains(member));
+            usedElements.add(member);
+        }
+        Assertions.assertEquals(new HashSet<>(set), usedElements);
+    }
+
+    @TestTemplate
+    void randMemberReturnsOnlyElementOfTheSet(Jedis jedis) {
+        Set<String> set = Collections.singleton("d");
+        jedis.sadd("foo", set.toArray(new String[0]));
+        for (int i = 0; i < 100; i++) {
+            Assertions.assertEquals("d", jedis.srandmember("foo"));
+        }
+    }
+
+    @TestTemplate
+    void randMemberReturnsNullForAnEmptySet(Jedis jedis) {
+        for (int i = 0; i < 100; i++) {
+            Assertions.assertNull(jedis.srandmember("foo"));
+        }
+    }
+
+    @TestTemplate
+    void randMemberReturnsDistinctElements(Jedis jedis) {
+        Collection<String> set = Arrays.asList("a", "b", "c", "d", "e");
+        jedis.sadd("foo", set.toArray(new String[0]));
+        for (int i = 0; i < 1000; i++) {
+            List<String> members = jedis.srandmember("foo", 3);
+            Assertions.assertTrue(set.containsAll(members));
+            Assertions.assertEquals(3, new HashSet<>(members).size());
+        }
+    }
+
+    @TestTemplate
+    void randMemberReturnsAllElements(Jedis jedis) {
+        Collection<String> set = Arrays.asList("a", "b", "c", "d", "e");
+        jedis.sadd("foo", set.toArray(new String[0]));
+        List<String> members = jedis.srandmember("foo", 10);
+        Assertions.assertEquals(new HashSet<>(set), new HashSet<>(members));
+    }
+
+    @TestTemplate
+    void randMemberReturnsNoElements(Jedis jedis) {
+        Collection<String> set = Arrays.asList("a", "b", "c", "d", "e");
+        jedis.sadd("foo", set.toArray(new String[0]));
+        List<String> members = jedis.srandmember("foo", 0);
+        Assertions.assertEquals(0, members.size());
+    }
+
+    @TestTemplate
+    void randMemberReturnsRepeatedElements(Jedis jedis) {
+        Collection<String> set = Arrays.asList("a", "b", "c");
+        jedis.sadd("foo", set.toArray(new String[0]));
+        List<String> members = jedis.srandmember("foo", -100);
+        Assertions.assertEquals(100, members.size());
+        Assertions.assertTrue(set.containsAll(members));
+    }
+}
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/sets/TestSScan.java b/src/test/java/com/github/fppt/jedismock/comparisontests/sets/SScanTest.java
similarity index 99%
rename from src/test/java/com/github/fppt/jedismock/comparisontests/sets/TestSScan.java
rename to src/test/java/com/github/fppt/jedismock/comparisontests/sets/SScanTest.java
index 4f592cf..5c2d81b 100644
--- a/src/test/java/com/github/fppt/jedismock/comparisontests/sets/TestSScan.java
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/sets/SScanTest.java
@@ -15,7 +15,7 @@ import java.util.Set;
 import static org.junit.jupiter.api.Assertions.*;
 
 @ExtendWith(ComparisonBase.class)
-public class TestSScan {
+public class SScanTest {
     private static final String key = "sscankey";
 
     @BeforeEach
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/sets/SUnionSUnionStoreTest.java b/src/test/java/com/github/fppt/jedismock/comparisontests/sets/SUnionSUnionStoreTest.java
new file mode 100644
index 0000000..03124fc
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/sets/SUnionSUnionStoreTest.java
@@ -0,0 +1,78 @@
+package com.github.fppt.jedismock.comparisontests.sets;
+
+import com.github.fppt.jedismock.comparisontests.ComparisonBase;
+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 java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@ExtendWith(ComparisonBase.class)
+public class SUnionSUnionStoreTest {
+    @BeforeEach
+    public void setUp(Jedis jedis) {
+        jedis.flushAll();
+    }
+
+    @TestTemplate
+    public void sUnionTest(Jedis jedis) {
+        String key1 = "set1";
+        String key2 = "set2";
+        String key3 = "set3";
+        Set<String> mySet1 = new HashSet<>(Arrays.asList("a", "b", "c", "d"));
+        Set<String> mySet2 = new HashSet<>(Collections.singletonList("c"));
+        Set<String> mySet3 = new HashSet<>(Arrays.asList("a", "c", "e", "f"));
+
+        Set<String> expectedUnion = new HashSet<>(Arrays.asList("a", "b", "c", "d", "e", "f"));
+
+        //Add everything from the sets
+        mySet1.forEach(value -> jedis.sadd(key1, value));
+        mySet2.forEach(value -> jedis.sadd(key2, value));
+        mySet3.forEach(value -> jedis.sadd(key3, value));
+
+
+        Set<String> result = jedis.sunion(key1, key2, key3);
+        assertEquals(6, result.size());
+        assertEquals(expectedUnion, result);
+    }
+
+    @TestTemplate
+    public void sUnionStoreTest(Jedis jedis) {
+        String key1 = "set1";
+        String key2 = "set2";
+        String key3 = "set3";
+        Set<String> mySet1 = new HashSet<>(Arrays.asList("a", "b", "c", "d"));
+        Set<String> mySet2 = new HashSet<>(Collections.singletonList("c"));
+        Set<String> mySet3 = new HashSet<>(Arrays.asList("a", "c", "e", "f"));
+
+        Set<String> expectedUnion = new HashSet<>(Arrays.asList("a", "b", "c", "d", "e", "f"));
+
+        //Add everything from the sets
+        mySet1.forEach(value -> jedis.sadd(key1, value));
+        mySet2.forEach(value -> jedis.sadd(key2, value));
+        mySet3.forEach(value -> jedis.sadd(key3, value));
+
+        String destination = "set3";
+
+        Long elementsInUnion = jedis.sunionstore(destination, key1, key2, key3);
+        assertEquals(6, elementsInUnion);
+
+        assertEquals(expectedUnion, jedis.smembers(destination));
+    }
+
+    @TestTemplate
+    public void deletesDestinationIfResultIsEmpty(Jedis jedis) {
+        jedis.sadd("dest", "a", "b");
+        assertTrue(jedis.exists("dest"));
+        jedis.sinterstore("dest", "src", "other");
+        assertFalse(jedis.exists("dest"));
+    }
+}
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/sets/SetOperationsTest.java b/src/test/java/com/github/fppt/jedismock/comparisontests/sets/SetOperationsTest.java
index a00680b..39424e8 100644
--- a/src/test/java/com/github/fppt/jedismock/comparisontests/sets/SetOperationsTest.java
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/sets/SetOperationsTest.java
@@ -95,6 +95,15 @@ public class SetOperationsTest {
         } while (poppedValue != null);
     }
 
+    @TestTemplate
+    public void poppingManyKeys(Jedis jedis) {
+        String key = "my-set-key-spop";
+        jedis.sadd(key, "a", "b", "c", "d");
+        assertEquals(3,
+                jedis.spop(key, 3).size());
+        assertEquals(1, jedis.scard(key));
+    }
+
     @TestTemplate
     public void ensureSismemberReturnsCorrectValues(Jedis jedis) {
         String key = "my-set-key-sismember";
@@ -104,29 +113,6 @@ public class SetOperationsTest {
         assertFalse(jedis.sismember(key + "-nonexistent", "A"));
     }
 
-    @TestTemplate
-    public void whenUsingHsinter_EnsureSetIntersectionIsReturned(Jedis jedis) {
-        String key1 = "my-set-key-1";
-        Set<String> mySet1 = new HashSet<>(Arrays.asList("a", "b", "c", "d"));
-        String key2 = "my-set-key-2";
-        Set<String> mySet2 = new HashSet<>(Arrays.asList("b", "d", "e", "f"));
-        String key3 = "my-set-key-3";
-        Set<String> mySet3 = new HashSet<>(Arrays.asList("b", "e", "f"));
-
-        Set<String> expectedIntersection1 = new HashSet<>(Arrays.asList("b", "d"));
-        Set<String> expectedIntersection2 = new HashSet<>(Collections.singletonList("b"));
-
-        //Add everything from the sets
-        mySet1.forEach(value -> jedis.sadd(key1, value));
-        mySet2.forEach(value -> jedis.sadd(key2, value));
-        mySet3.forEach(value -> jedis.sadd(key3, value));
-
-        Set<String> intersection = jedis.sinter(key1, key2);
-        assertEquals(expectedIntersection1, intersection);
-
-        intersection = jedis.sinter(key1, key2, key3);
-        assertEquals(expectedIntersection2, intersection);
-    }
 
     @TestTemplate
     public void testFailingGetOperation(Jedis jedis) {
@@ -143,4 +129,42 @@ public class SetOperationsTest {
         byte[] newMsg = jedis.spop("foo".getBytes());
         assertArrayEquals(msg, newMsg);
     }
+
+    @TestTemplate
+    public void testSMoveExistingElement(Jedis jedis) {
+        jedis.sadd("myset", "one", "two");
+        jedis.sadd("myotherset", "three");
+        assertEquals(1, jedis.smove("myset", "myotherset", "two"));
+        assertEquals(Collections.singleton("one"), jedis.smembers("myset"));
+        assertEquals(new HashSet<>(Arrays.asList("two", "three")),
+                jedis.smembers("myotherset"));
+    }
+
+    @TestTemplate
+    public void testSMoveNonExistingElement(Jedis jedis) {
+        jedis.sadd("myset", "one", "two");
+        jedis.sadd("myotherset", "three");
+        assertEquals(0, jedis.smove("myset", "myotherset", "four"));
+        assertEquals(new HashSet<>(Arrays.asList("one", "two")),
+                jedis.smembers("myset"));
+        assertEquals(Collections.singleton("three"),
+                jedis.smembers("myotherset"));
+    }
+
+    @TestTemplate
+    public void testSMoveWrongTypesSrcDest(Jedis jedis) {
+
+        String key1 = "key1";
+        String key2 = "key2";
+        
+        jedis.set(key1, "a");
+        jedis.sadd(key2, "b");
+
+        assertTrue(
+                assertThrows(JedisDataException.class, () -> jedis.smove(key1, key2, "a"))
+                        .getMessage().startsWith("WRONGTYPE"));
+        assertTrue(
+                assertThrows(JedisDataException.class, () -> jedis.smove(key2, key1, "a"))
+                        .getMessage().startsWith("WRONGTYPE"));
+    }
 }
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/sortedsets/TestZRangeByLex.java b/src/test/java/com/github/fppt/jedismock/comparisontests/sortedsets/TestZRangeByLex.java
index 0fdbbe0..c364081 100644
--- a/src/test/java/com/github/fppt/jedismock/comparisontests/sortedsets/TestZRangeByLex.java
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/sortedsets/TestZRangeByLex.java
@@ -61,4 +61,22 @@ public class TestZRangeByLex {
         assertThrows(JedisDataException.class, () -> jedis.zrangeByLex(key, "[b", "d"));
         assertThrows(JedisDataException.class, () -> jedis.zrevrangeByLex(key, "[b", "d"));
     }
+
+    @TestTemplate
+    public void zrangebylexLimit(Jedis jedis) {
+        assertEquals(Collections.singletonList("aaa"),
+                jedis.zrangeByLex(key, "[a", "(c", 0, 1));
+    }
+
+    @TestTemplate
+    public void zrevrangebylexLimit(Jedis jedis) {
+        assertEquals(Collections.singletonList("bbb"),
+                jedis.zrevrangeByLex(key, "(c", "[a", 0, 1));
+    }
+
+    @TestTemplate
+    public void zrangebylexNegativeLimit(Jedis jedis) {
+        assertEquals(Arrays.asList("aaa", "bbb"),
+                jedis.zrangeByLex(key, "[a", "(c", 0, -1));
+    }
 }
diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/sortedsets/TestZRangeByScore.java b/src/test/java/com/github/fppt/jedismock/comparisontests/sortedsets/TestZRangeByScore.java
index a093b22..764f985 100644
--- a/src/test/java/com/github/fppt/jedismock/comparisontests/sortedsets/TestZRangeByScore.java
+++ b/src/test/java/com/github/fppt/jedismock/comparisontests/sortedsets/TestZRangeByScore.java
@@ -9,6 +9,7 @@ import redis.clients.jedis.exceptions.JedisDataException;
 import redis.clients.jedis.resps.Tuple;
 
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 
 import static java.util.Arrays.asList;
@@ -42,7 +43,7 @@ public class TestZRangeByScore {
         assertTrue(asList("one", "two", "three").containsAll(
                 jedis.zrangeByScore(ZSET_KEY, "-inf", "+inf")));
         assertTrue(asList(new Tuple("one", 1.),
-                        new Tuple("two", 1.), new Tuple("three", 1.)).containsAll(
+                new Tuple("two", 1.), new Tuple("three", 1.)).containsAll(
                 jedis.zrangeByScoreWithScores(ZSET_KEY, "-inf", "+inf")));
     }
 
@@ -205,4 +206,11 @@ public class TestZRangeByScore {
         jedis.zadd("foo", 42, "bar");
         assertEquals(0, jedis.zrangeByScore("foo", 5, 2).size());
     }
+
+    @TestTemplate
+    void negativeCount(Jedis jedis) {
+        jedis.zadd("foo", 17, "bar");
+        assertEquals(Collections.singletonList("bar"),
+                jedis.zrangeByScore("foo", 0, 42, 0, -1));
+    }
 }