Repository: redis/redis-py
Pull request #2088 (2022-04-05 21:23:33)

Fix typing in getex command
### Pull Request check-list

_Please make sure to review and check all of these items:_

- [ ] Does `$ tox` pass with this change (including linting)?
- [ ] Do the CI tests pass with this change (enable it first in your forked repo and wait for the github action build to finish)?
- [ ] Is the new or changed code fully tested?
- [ ] Is a documentation update included (if this change modifies existing APIs, or introduces new ones)?
- [ ] Is there an example added to the examples folder (if applicable)?
- [ ] Was the change added to CHANGES file?

_NOTE: these things are not required to open a PR and can be done
afterwards / while the PR is open._

### Description of change

_Please provide a description of the change here._


codecov-commenter: # [Codecov](https://codecov.io/gh/redis/redis-py/pull/2088?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis) Report
> Merging [#2088](https://codecov.io/gh/redis/redis-py/pull/2088?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis) (e360469) into [master](https://codecov.io/gh/redis/redis-py/commit/20e4b873151804f596fa324c38f18fae175c2e63?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis) (20e4b87) will **decrease** coverage by `0.05%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##           master    #2088      +/-   ##
==========================================
- Coverage   91.22%   91.17%   -0.06%     
==========================================
  Files         100      100              
  Lines       21486    21485       -1     
==========================================
- Hits        19601    19589      -12     
- Misses       1885     1896      +11     
```


| [Impacted Files](https://codecov.io/gh/redis/redis-py/pull/2088?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis) | Coverage Δ | |
|---|---|---|
| [redis/commands/core.py](https://codecov.io/gh/redis/redis-py/pull/2088/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis#diff-cmVkaXMvY29tbWFuZHMvY29yZS5weQ==) | `82.85% <100.00%> (ø)` | |
| [tests/test\_cluster.py](https://codecov.io/gh/redis/redis-py/pull/2088/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis#diff-dGVzdHMvdGVzdF9jbHVzdGVyLnB5) | `97.12% <0.00%> (-0.48%)` | :arrow_down: |
| [redis/asyncio/connection.py](https://codecov.io/gh/redis/redis-py/pull/2088/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis#diff-cmVkaXMvYXN5bmNpby9jb25uZWN0aW9uLnB5) | `82.52% <0.00%> (-0.35%)` | :arrow_down: |
| [tests/test\_graph.py](https://codecov.io/gh/redis/redis-py/pull/2088/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis#diff-dGVzdHMvdGVzdF9ncmFwaC5weQ==) | `90.59% <0.00%> (-0.04%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/redis/redis-py/pull/2088?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/redis/redis-py/pull/2088?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis). Last update [20e4b87...e360469](https://codecov.io/gh/redis/redis-py/pull/2088?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis).


Patch:
diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml
index 74b25d6b..53e67770 100644
--- a/.github/workflows/integration.yaml
+++ b/.github/workflows/integration.yaml
@@ -22,7 +22,7 @@ jobs:
      steps:
        - uses: actions/checkout@v2
        - name: install python
-         uses: actions/setup-python@v2
+         uses: actions/setup-python@v3
          with:
            python-version: 3.9
        - name: run code linters
@@ -32,6 +32,7 @@ jobs:
 
    run-tests:
      runs-on: ubuntu-latest
+     continue-on-error: ${{ matrix.experimental }}
      timeout-minutes: 30
      strategy:
        max-parallel: 15
@@ -39,13 +40,19 @@ jobs:
          python-version: ['3.6', '3.7', '3.8', '3.9', '3.10', 'pypy-3.7']
          test-type: ['standalone', 'cluster']
          connection-type: ['hiredis', 'plain']
+         experimental: [false]
+         include:
+           - python-version: 3.11.0-alpha.6
+             experimental: true
+             test-type: standalone
+             connection-type: plain
      env:
        ACTIONS_ALLOW_UNSECURE_COMMANDS: true
      name: Python ${{ matrix.python-version }} ${{matrix.test-type}}-${{matrix.connection-type}} tests
      steps:
        - uses: actions/checkout@v2
        - name: install python
-         uses: actions/setup-python@v2
+         uses: actions/setup-python@v3
          with:
            python-version: ${{ matrix.python-version }}
        - name: run tests
@@ -68,7 +75,7 @@ jobs:
     steps:
       - uses: actions/checkout@v2
       - name: install python
-        uses: actions/setup-python@v2
+        uses: actions/setup-python@v3
         with:
           python-version: 3.9
       - name: Run installed unit tests
@@ -76,15 +83,20 @@ jobs:
           bash .github/workflows/install_and_test.sh ${{ matrix.extension }}
 
    install_package_from_commit:
+    continue-on-error: ${{ matrix.experimental }}
     name: Install package from commit hash
     runs-on: ubuntu-latest
     strategy:
       matrix:
         python-version: ['3.6', '3.7', '3.8', '3.9', '3.10', 'pypy-3.7']
+        experimental: [false]
+        include:
+          - python-version: 3.11.0-alpha.5
+          - experimental: true
     steps:
       - uses: actions/checkout@v2
       - name: install python ${{ matrix.python-version }}
-        uses: actions/setup-python@v2
+        uses: actions/setup-python@v3
         with:
           python-version: ${{ matrix.python-version }}
       - name: install from pip
diff --git a/.github/workflows/pypi-publish.yaml b/.github/workflows/pypi-publish.yaml
index 3cccb067..0d100dd1 100644
--- a/.github/workflows/pypi-publish.yaml
+++ b/.github/workflows/pypi-publish.yaml
@@ -11,7 +11,7 @@ jobs:
     steps:
       - uses: actions/checkout@v2
       - name: install python
-        uses: actions/setup-python@v2
+        uses: actions/setup-python@v3
         with:
           python-version: 3.9
       - name: Install dev tools
diff --git a/redis/asyncio/client.py b/redis/asyncio/client.py
index 7175e029..559b74c9 100644
--- a/redis/asyncio/client.py
+++ b/redis/asyncio/client.py
@@ -255,6 +255,14 @@ class Redis(
         """Set a custom Response Callback"""
         self.response_callbacks[command] = callback
 
+    def get_encoder(self):
+        """Get the connection pool's encoder"""
+        return self.connection_pool.get_encoder()
+
+    def get_connection_kwargs(self):
+        """Get the connection's key-word arguments"""
+        return self.connection_pool.connection_kwargs
+
     def load_external_module(
         self,
         funcname,
diff --git a/redis/commands/core.py b/redis/commands/core.py
index 606256a3..d1d465d7 100644
--- a/redis/commands/core.py
+++ b/redis/commands/core.py
@@ -256,7 +256,7 @@ class ACLCommands(CommandsProtocol):
         For more information see https://redis.io/commands/acl-setuser
         """
         encoder = self.get_encoder()
-        pieces: list[str | bytes] = [username]
+        pieces: List[EncodableT] = [username]
 
         if reset:
             pieces.append(b"reset")
diff --git a/setup.py b/setup.py
index 423117bb..ebb71f8e 100644
--- a/setup.py
+++ b/setup.py
@@ -52,6 +52,7 @@ setup(
         "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: 3.9",
         "Programming Language :: Python :: 3.10",
+        "Programming Language :: Python :: 3.11",
         "Programming Language :: Python :: Implementation :: CPython",
         "Programming Language :: Python :: Implementation :: PyPy",
     ],
diff --git a/tox.ini b/tox.ini
index 9ea8706f..a880da45 100644
--- a/tox.ini
+++ b/tox.ini
@@ -6,9 +6,9 @@ markers =
     onlycluster: marks tests to be run only with cluster mode redis
     onlynoncluster: marks tests to be run only with standalone redis
     ssl: marker for only the ssl tests
+    asyncio: marker for async tests
     replica: replica tests
     experimental: run only experimental tests
-    asyncio: marker for async tests
 
 [tox]
 minversion = 3.2.0


Test patch:
diff --git a/tests/test_asyncio/conftest.py b/tests/test_asyncio/conftest.py
index 0e9c73ec..34c22c5a 100644
--- a/tests/test_asyncio/conftest.py
+++ b/tests/test_asyncio/conftest.py
@@ -20,6 +20,8 @@ from redis.asyncio.connection import (
     PythonParser,
     parse_url,
 )
+from redis.asyncio.retry import Retry
+from redis.backoff import NoBackoff
 from tests.conftest import REDIS_INFO
 
 from .compat import mock
@@ -97,7 +99,7 @@ def create_redis(request, event_loop: asyncio.BaseEventLoop):
 
 
 @pytest_asyncio.fixture()
-async def r(create_redis):
+async def r(request, create_redis):
     yield await create_redis()
 
 
@@ -107,8 +109,16 @@ async def r2(create_redis):
     yield await create_redis()
 
 
+@pytest_asyncio.fixture()
+async def modclient(request, create_redis):
+    yield await create_redis(
+        url=request.config.getoption("--redismod-url"), decode_responses=True
+    )
+
+
 def _gen_cluster_mock_resp(r, response):
     connection = mock.AsyncMock()
+    connection.retry = Retry(NoBackoff(), 0)
     connection.read_response.return_value = response
     r.connection = connection
     return r
diff --git a/tests/test_asyncio/test_bloom.py b/tests/test_asyncio/test_bloom.py
new file mode 100644
index 00000000..feb98cc4
--- /dev/null
+++ b/tests/test_asyncio/test_bloom.py
@@ -0,0 +1,402 @@
+import pytest
+
+import redis.asyncio as redis
+from redis.exceptions import ModuleError, RedisError
+from redis.utils import HIREDIS_AVAILABLE
+
+pytestmark = pytest.mark.asyncio
+
+
+def intlist(obj):
+    return [int(v) for v in obj]
+
+
+# @pytest.fixture
+# async def client(modclient):
+#     assert isinstance(modawait modclient.bf(), redis.commands.bf.BFBloom)
+#     assert isinstance(modawait modclient.cf(), redis.commands.bf.CFBloom)
+#     assert isinstance(modawait modclient.cms(), redis.commands.bf.CMSBloom)
+#     assert isinstance(modawait modclient.tdigest(), redis.commands.bf.TDigestBloom)
+#     assert isinstance(modawait modclient.topk(), redis.commands.bf.TOPKBloom)
+
+#     modawait modclient.flushdb()
+#     return modclient
+
+
+@pytest.mark.redismod
+async def test_create(modclient: redis.Redis):
+    """Test CREATE/RESERVE calls"""
+    assert await modclient.bf().create("bloom", 0.01, 1000)
+    assert await modclient.bf().create("bloom_e", 0.01, 1000, expansion=1)
+    assert await modclient.bf().create("bloom_ns", 0.01, 1000, noScale=True)
+    assert await modclient.cf().create("cuckoo", 1000)
+    assert await modclient.cf().create("cuckoo_e", 1000, expansion=1)
+    assert await modclient.cf().create("cuckoo_bs", 1000, bucket_size=4)
+    assert await modclient.cf().create("cuckoo_mi", 1000, max_iterations=10)
+    assert await modclient.cms().initbydim("cmsDim", 100, 5)
+    assert await modclient.cms().initbyprob("cmsProb", 0.01, 0.01)
+    assert await modclient.topk().reserve("topk", 5, 100, 5, 0.9)
+
+
+@pytest.mark.redismod
+@pytest.mark.experimental
+async def test_tdigest_create(modclient: redis.Redis):
+    assert await modclient.tdigest().create("tDigest", 100)
+
+
+# region Test Bloom Filter
+@pytest.mark.redismod
+async def test_bf_add(modclient: redis.Redis):
+    assert await modclient.bf().create("bloom", 0.01, 1000)
+    assert 1 == await modclient.bf().add("bloom", "foo")
+    assert 0 == await modclient.bf().add("bloom", "foo")
+    assert [0] == intlist(await modclient.bf().madd("bloom", "foo"))
+    assert [0, 1] == await modclient.bf().madd("bloom", "foo", "bar")
+    assert [0, 0, 1] == await modclient.bf().madd("bloom", "foo", "bar", "baz")
+    assert 1 == await modclient.bf().exists("bloom", "foo")
+    assert 0 == await modclient.bf().exists("bloom", "noexist")
+    assert [1, 0] == intlist(await modclient.bf().mexists("bloom", "foo", "noexist"))
+
+
+@pytest.mark.redismod
+async def test_bf_insert(modclient: redis.Redis):
+    assert await modclient.bf().create("bloom", 0.01, 1000)
+    assert [1] == intlist(await modclient.bf().insert("bloom", ["foo"]))
+    assert [0, 1] == intlist(await modclient.bf().insert("bloom", ["foo", "bar"]))
+    assert [1] == intlist(await modclient.bf().insert("captest", ["foo"], capacity=10))
+    assert [1] == intlist(await modclient.bf().insert("errtest", ["foo"], error=0.01))
+    assert 1 == await modclient.bf().exists("bloom", "foo")
+    assert 0 == await modclient.bf().exists("bloom", "noexist")
+    assert [1, 0] == intlist(await modclient.bf().mexists("bloom", "foo", "noexist"))
+    info = await modclient.bf().info("bloom")
+    assert 2 == info.insertedNum
+    assert 1000 == info.capacity
+    assert 1 == info.filterNum
+
+
+@pytest.mark.redismod
+async def test_bf_scandump_and_loadchunk(modclient: redis.Redis):
+    # Store a filter
+    await modclient.bf().create("myBloom", "0.0001", "1000")
+
+    # test is probabilistic and might fail. It is OK to change variables if
+    # certain to not break anything
+    async def do_verify():
+        res = 0
+        for x in range(1000):
+            await modclient.bf().add("myBloom", x)
+            rv = await modclient.bf().exists("myBloom", x)
+            assert rv
+            rv = await modclient.bf().exists("myBloom", f"nonexist_{x}")
+            res += rv == x
+        assert res < 5
+
+    do_verify()
+    cmds = []
+    if HIREDIS_AVAILABLE:
+        with pytest.raises(ModuleError):
+            cur = await modclient.bf().scandump("myBloom", 0)
+        return
+
+    cur = await modclient.bf().scandump("myBloom", 0)
+    first = cur[0]
+    cmds.append(cur)
+
+    while True:
+        cur = await modclient.bf().scandump("myBloom", first)
+        first = cur[0]
+        if first == 0:
+            break
+        else:
+            cmds.append(cur)
+    prev_info = await modclient.bf().execute_command("bf.debug", "myBloom")
+
+    # Remove the filter
+    await modclient.bf().client.delete("myBloom")
+
+    # Now, load all the commands:
+    for cmd in cmds:
+        await modclient.bf().loadchunk("myBloom", *cmd)
+
+    cur_info = await modclient.bf().execute_command("bf.debug", "myBloom")
+    assert prev_info == cur_info
+    do_verify()
+
+    await modclient.bf().client.delete("myBloom")
+    await modclient.bf().create("myBloom", "0.0001", "10000000")
+
+
+@pytest.mark.redismod
+async def test_bf_info(modclient: redis.Redis):
+    expansion = 4
+    # Store a filter
+    await modclient.bf().create("nonscaling", "0.0001", "1000", noScale=True)
+    info = await modclient.bf().info("nonscaling")
+    assert info.expansionRate is None
+
+    await modclient.bf().create("expanding", "0.0001", "1000", expansion=expansion)
+    info = await modclient.bf().info("expanding")
+    assert info.expansionRate == 4
+
+    try:
+        # noScale mean no expansion
+        await modclient.bf().create(
+            "myBloom", "0.0001", "1000", expansion=expansion, noScale=True
+        )
+        assert False
+    except RedisError:
+        assert True
+
+
+# region Test Cuckoo Filter
+@pytest.mark.redismod
+async def test_cf_add_and_insert(modclient: redis.Redis):
+    assert await modclient.cf().create("cuckoo", 1000)
+    assert await modclient.cf().add("cuckoo", "filter")
+    assert not await modclient.cf().addnx("cuckoo", "filter")
+    assert 1 == await modclient.cf().addnx("cuckoo", "newItem")
+    assert [1] == await modclient.cf().insert("captest", ["foo"])
+    assert [1] == await modclient.cf().insert("captest", ["foo"], capacity=1000)
+    assert [1] == await modclient.cf().insertnx("captest", ["bar"])
+    assert [1] == await modclient.cf().insertnx("captest", ["food"], nocreate="1")
+    assert [0, 0, 1] == await modclient.cf().insertnx("captest", ["foo", "bar", "baz"])
+    assert [0] == await modclient.cf().insertnx("captest", ["bar"], capacity=1000)
+    assert [1] == await modclient.cf().insert("empty1", ["foo"], capacity=1000)
+    assert [1] == await modclient.cf().insertnx("empty2", ["bar"], capacity=1000)
+    info = await modclient.cf().info("captest")
+    assert 5 == info.insertedNum
+    assert 0 == info.deletedNum
+    assert 1 == info.filterNum
+
+
+@pytest.mark.redismod
+async def test_cf_exists_and_del(modclient: redis.Redis):
+    assert await modclient.cf().create("cuckoo", 1000)
+    assert await modclient.cf().add("cuckoo", "filter")
+    assert await modclient.cf().exists("cuckoo", "filter")
+    assert not await modclient.cf().exists("cuckoo", "notexist")
+    assert 1 == await modclient.cf().count("cuckoo", "filter")
+    assert 0 == await modclient.cf().count("cuckoo", "notexist")
+    assert await modclient.cf().delete("cuckoo", "filter")
+    assert 0 == await modclient.cf().count("cuckoo", "filter")
+
+
+# region Test Count-Min Sketch
+@pytest.mark.redismod
+async def test_cms(modclient: redis.Redis):
+    assert await modclient.cms().initbydim("dim", 1000, 5)
+    assert await modclient.cms().initbyprob("prob", 0.01, 0.01)
+    assert await modclient.cms().incrby("dim", ["foo"], [5])
+    assert [0] == await modclient.cms().query("dim", "notexist")
+    assert [5] == await modclient.cms().query("dim", "foo")
+    assert [10, 15] == await modclient.cms().incrby("dim", ["foo", "bar"], [5, 15])
+    assert [10, 15] == await modclient.cms().query("dim", "foo", "bar")
+    info = await modclient.cms().info("dim")
+    assert 1000 == info.width
+    assert 5 == info.depth
+    assert 25 == info.count
+
+
+@pytest.mark.redismod
+@pytest.mark.onlynoncluster
+async def test_cms_merge(modclient: redis.Redis):
+    assert await modclient.cms().initbydim("A", 1000, 5)
+    assert await modclient.cms().initbydim("B", 1000, 5)
+    assert await modclient.cms().initbydim("C", 1000, 5)
+    assert await modclient.cms().incrby("A", ["foo", "bar", "baz"], [5, 3, 9])
+    assert await modclient.cms().incrby("B", ["foo", "bar", "baz"], [2, 3, 1])
+    assert [5, 3, 9] == await modclient.cms().query("A", "foo", "bar", "baz")
+    assert [2, 3, 1] == await modclient.cms().query("B", "foo", "bar", "baz")
+    assert await modclient.cms().merge("C", 2, ["A", "B"])
+    assert [7, 6, 10] == await modclient.cms().query("C", "foo", "bar", "baz")
+    assert await modclient.cms().merge("C", 2, ["A", "B"], ["1", "2"])
+    assert [9, 9, 11] == await modclient.cms().query("C", "foo", "bar", "baz")
+    assert await modclient.cms().merge("C", 2, ["A", "B"], ["2", "3"])
+    assert [16, 15, 21] == await modclient.cms().query("C", "foo", "bar", "baz")
+
+
+# endregion
+
+
+# region Test Top-K
+@pytest.mark.redismod
+async def test_topk(modclient: redis.Redis):
+    # test list with empty buckets
+    assert await modclient.topk().reserve("topk", 3, 50, 4, 0.9)
+    assert [
+        None,
+        None,
+        None,
+        "A",
+        "C",
+        "D",
+        None,
+        None,
+        "E",
+        None,
+        "B",
+        "C",
+        None,
+        None,
+        None,
+        "D",
+        None,
+    ] == await modclient.topk().add(
+        "topk",
+        "A",
+        "B",
+        "C",
+        "D",
+        "E",
+        "A",
+        "A",
+        "B",
+        "C",
+        "G",
+        "D",
+        "B",
+        "D",
+        "A",
+        "E",
+        "E",
+        1,
+    )
+    assert [1, 1, 0, 0, 1, 0, 0] == await modclient.topk().query(
+        "topk", "A", "B", "C", "D", "E", "F", "G"
+    )
+    assert [4, 3, 2, 3, 3, 0, 1] == await modclient.topk().count(
+        "topk", "A", "B", "C", "D", "E", "F", "G"
+    )
+
+    # test full list
+    assert await modclient.topk().reserve("topklist", 3, 50, 3, 0.9)
+    assert await modclient.topk().add(
+        "topklist",
+        "A",
+        "B",
+        "C",
+        "D",
+        "E",
+        "A",
+        "A",
+        "B",
+        "C",
+        "G",
+        "D",
+        "B",
+        "D",
+        "A",
+        "E",
+        "E",
+    )
+    assert ["A", "B", "E"] == await modclient.topk().list("topklist")
+    res = await modclient.topk().list("topklist", withcount=True)
+    assert ["A", 4, "B", 3, "E", 3] == res
+    info = await modclient.topk().info("topklist")
+    assert 3 == info.k
+    assert 50 == info.width
+    assert 3 == info.depth
+    assert 0.9 == round(float(info.decay), 1)
+
+
+@pytest.mark.redismod
+async def test_topk_incrby(modclient: redis.Redis):
+    await modclient.flushdb()
+    assert await modclient.topk().reserve("topk", 3, 10, 3, 1)
+    assert [None, None, None] == await modclient.topk().incrby(
+        "topk", ["bar", "baz", "42"], [3, 6, 2]
+    )
+    res = await modclient.topk().incrby("topk", ["42", "xyzzy"], [8, 4])
+    assert [None, "bar"] == res
+    assert [3, 6, 10, 4, 0] == await modclient.topk().count(
+        "topk", "bar", "baz", "42", "xyzzy", 4
+    )
+
+
+# region Test T-Digest
+@pytest.mark.redismod
+@pytest.mark.experimental
+async def test_tdigest_reset(modclient: redis.Redis):
+    assert await modclient.tdigest().create("tDigest", 10)
+    # reset on empty histogram
+    assert await modclient.tdigest().reset("tDigest")
+    # insert data-points into sketch
+    assert await modclient.tdigest().add("tDigest", list(range(10)), [1.0] * 10)
+
+    assert await modclient.tdigest().reset("tDigest")
+    # assert we have 0 unmerged nodes
+    assert 0 == (await modclient.tdigest().info("tDigest")).unmergedNodes
+
+
+@pytest.mark.redismod
+@pytest.mark.experimental
+async def test_tdigest_merge(modclient: redis.Redis):
+    assert await modclient.tdigest().create("to-tDigest", 10)
+    assert await modclient.tdigest().create("from-tDigest", 10)
+    # insert data-points into sketch
+    assert await modclient.tdigest().add("from-tDigest", [1.0] * 10, [1.0] * 10)
+    assert await modclient.tdigest().add("to-tDigest", [2.0] * 10, [10.0] * 10)
+    # merge from-tdigest into to-tdigest
+    assert await modclient.tdigest().merge("to-tDigest", "from-tDigest")
+    # we should now have 110 weight on to-histogram
+    info = await modclient.tdigest().info("to-tDigest")
+    total_weight_to = float(info.mergedWeight) + float(info.unmergedWeight)
+    assert 110 == total_weight_to
+
+
+@pytest.mark.redismod
+@pytest.mark.experimental
+async def test_tdigest_min_and_max(modclient: redis.Redis):
+    assert await modclient.tdigest().create("tDigest", 100)
+    # insert data-points into sketch
+    assert await modclient.tdigest().add("tDigest", [1, 2, 3], [1.0] * 3)
+    # min/max
+    assert 3 == await modclient.tdigest().max("tDigest")
+    assert 1 == await modclient.tdigest().min("tDigest")
+
+
+@pytest.mark.redismod
+@pytest.mark.experimental
+async def test_tdigest_quantile(modclient: redis.Redis):
+    assert await modclient.tdigest().create("tDigest", 500)
+    # insert data-points into sketch
+    assert await modclient.tdigest().add(
+        "tDigest", list([x * 0.01 for x in range(1, 10000)]), [1.0] * 10000
+    )
+    # assert min min/max have same result as quantile 0 and 1
+    assert await modclient.tdigest().max(
+        "tDigest"
+    ) == await modclient.tdigest().quantile("tDigest", 1.0)
+    assert await modclient.tdigest().min(
+        "tDigest"
+    ) == await modclient.tdigest().quantile("tDigest", 0.0)
+
+    assert 1.0 == round(await modclient.tdigest().quantile("tDigest", 0.01), 2)
+    assert 99.0 == round(await modclient.tdigest().quantile("tDigest", 0.99), 2)
+
+
+@pytest.mark.redismod
+@pytest.mark.experimental
+async def test_tdigest_cdf(modclient: redis.Redis):
+    assert await modclient.tdigest().create("tDigest", 100)
+    # insert data-points into sketch
+    assert await modclient.tdigest().add("tDigest", list(range(1, 10)), [1.0] * 10)
+    assert 0.1 == round(await modclient.tdigest().cdf("tDigest", 1.0), 1)
+    assert 0.9 == round(await modclient.tdigest().cdf("tDigest", 9.0), 1)
+
+
+# @pytest.mark.redismod
+# async def test_pipeline(modclient: redis.Redis):
+#     pipeline = await modclient.bf().pipeline()
+#     assert not await modclient.bf().execute_command("get pipeline")
+#
+#     assert await modclient.bf().create("pipeline", 0.01, 1000)
+#     for i in range(100):
+#         pipeline.add("pipeline", i)
+#     for i in range(100):
+#         assert not (await modclient.bf().exists("pipeline", i))
+#
+#     pipeline.execute()
+#
+#     for i in range(100):
+#         assert await modclient.bf().exists("pipeline", i)
diff --git a/tests/test_asyncio/test_commands.py b/tests/test_asyncio/test_commands.py
index 12855c93..78220406 100644
--- a/tests/test_asyncio/test_commands.py
+++ b/tests/test_asyncio/test_commands.py
@@ -1,3 +1,3052 @@
 """
 Tests async overrides of commands from their mixins
 """
+import binascii
+import datetime
+import re
+import time
+from string import ascii_letters
+
+import pytest
+
+import redis
+from redis import exceptions
+from redis.client import parse_info
+from tests.conftest import (
+    skip_if_server_version_gte,
+    skip_if_server_version_lt,
+    skip_unless_arch_bits,
+)
+
+REDIS_6_VERSION = "5.9.0"
+
+
+pytestmark = [pytest.mark.asyncio, pytest.mark.onlynoncluster]
+
+
+@pytest.fixture()
+async def slowlog(r: redis.Redis, event_loop):
+    current_config = await r.config_get()
+    old_slower_than_value = current_config["slowlog-log-slower-than"]
+    old_max_legnth_value = current_config["slowlog-max-len"]
+
+    await r.config_set("slowlog-log-slower-than", 0)
+    await r.config_set("slowlog-max-len", 128)
+
+    yield
+
+    await r.config_set("slowlog-log-slower-than", old_slower_than_value)
+    await r.config_set("slowlog-max-len", old_max_legnth_value)
+
+
+async def redis_server_time(client: redis.Redis):
+    seconds, milliseconds = await client.time()
+    timestamp = float(f"{seconds}.{milliseconds}")
+    return datetime.datetime.fromtimestamp(timestamp)
+
+
+async def get_stream_message(client: redis.Redis, stream: str, message_id: str):
+    """Fetch a stream message and format it as a (message_id, fields) pair"""
+    response = await client.xrange(stream, min=message_id, max=message_id)
+    assert len(response) == 1
+    return response[0]
+
+
+# RESPONSE CALLBACKS
+class TestResponseCallbacks:
+    """Tests for the response callback system"""
+
+    async def test_response_callbacks(self, r: redis.Redis):
+        assert r.response_callbacks == redis.Redis.RESPONSE_CALLBACKS
+        assert id(r.response_callbacks) != id(redis.Redis.RESPONSE_CALLBACKS)
+        r.set_response_callback("GET", lambda x: "static")
+        await r.set("a", "foo")
+        assert await r.get("a") == "static"
+
+    async def test_case_insensitive_command_names(self, r: redis.Redis):
+        assert r.response_callbacks["del"] == r.response_callbacks["DEL"]
+
+
+class TestRedisCommands:
+    async def test_command_on_invalid_key_type(self, r: redis.Redis):
+        await r.lpush("a", "1")
+        with pytest.raises(redis.ResponseError):
+            await r.get("a")
+
+    # SERVER INFORMATION
+    @skip_if_server_version_lt(REDIS_6_VERSION)
+    async def test_acl_cat_no_category(self, r: redis.Redis):
+        categories = await r.acl_cat()
+        assert isinstance(categories, list)
+        assert "read" in categories
+
+    @skip_if_server_version_lt(REDIS_6_VERSION)
+    async def test_acl_cat_with_category(self, r: redis.Redis):
+        commands = await r.acl_cat("read")
+        assert isinstance(commands, list)
+        assert "get" in commands
+
+    @skip_if_server_version_lt(REDIS_6_VERSION)
+    async def test_acl_deluser(self, r: redis.Redis, request, event_loop):
+        username = "redis-py-user"
+
+        def teardown():
+            coro = r.acl_deluser(username)
+            if event_loop.is_running():
+                event_loop.create_task(coro)
+            else:
+                event_loop.run_until_complete(coro)
+
+        request.addfinalizer(teardown)
+
+        assert await r.acl_deluser(username) == 0
+        assert await r.acl_setuser(username, enabled=False, reset=True)
+        assert await r.acl_deluser(username) == 1
+
+    @skip_if_server_version_lt(REDIS_6_VERSION)
+    async def test_acl_genpass(self, r: redis.Redis):
+        password = await r.acl_genpass()
+        assert isinstance(password, str)
+
+    @skip_if_server_version_lt(REDIS_6_VERSION)
+    async def test_acl_getuser_setuser(self, r: redis.Redis, request, event_loop):
+        username = "redis-py-user"
+
+        def teardown():
+            coro = r.acl_deluser(username)
+            if event_loop.is_running():
+                event_loop.create_task(coro)
+            else:
+                event_loop.run_until_complete(coro)
+
+        request.addfinalizer(teardown)
+
+        # test enabled=False
+        assert await r.acl_setuser(username, enabled=False, reset=True)
+        assert await r.acl_getuser(username) == {
+            "categories": ["-@all"],
+            "commands": [],
+            "channels": [b"*"],
+            "enabled": False,
+            "flags": ["off", "allchannels", "sanitize-payload"],
+            "keys": [],
+            "passwords": [],
+        }
+
+        # test nopass=True
+        assert await r.acl_setuser(username, enabled=True, reset=True, nopass=True)
+        assert await r.acl_getuser(username) == {
+            "categories": ["-@all"],
+            "commands": [],
+            "channels": [b"*"],
+            "enabled": True,
+            "flags": ["on", "allchannels", "nopass", "sanitize-payload"],
+            "keys": [],
+            "passwords": [],
+        }
+
+        # test all args
+        assert await r.acl_setuser(
+            username,
+            enabled=True,
+            reset=True,
+            passwords=["+pass1", "+pass2"],
+            categories=["+set", "+@hash", "-geo"],
+            commands=["+get", "+mget", "-hset"],
+            keys=["cache:*", "objects:*"],
+        )
+        acl = await r.acl_getuser(username)
+        assert set(acl["categories"]) == {"-@all", "+@set", "+@hash"}
+        assert set(acl["commands"]) == {"+get", "+mget", "-hset"}
+        assert acl["enabled"] is True
+        assert acl["channels"] == [b"*"]
+        assert acl["flags"] == ["on", "allchannels", "sanitize-payload"]
+        assert set(acl["keys"]) == {b"cache:*", b"objects:*"}
+        assert len(acl["passwords"]) == 2
+
+        # test reset=False keeps existing ACL and applies new ACL on top
+        assert await r.acl_setuser(
+            username,
+            enabled=True,
+            reset=True,
+            passwords=["+pass1"],
+            categories=["+@set"],
+            commands=["+get"],
+            keys=["cache:*"],
+        )
+        assert await r.acl_setuser(
+            username,
+            enabled=True,
+            passwords=["+pass2"],
+            categories=["+@hash"],
+            commands=["+mget"],
+            keys=["objects:*"],
+        )
+        acl = await r.acl_getuser(username)
+        assert set(acl["categories"]) == {"-@all", "+@set", "+@hash"}
+        assert set(acl["commands"]) == {"+get", "+mget"}
+        assert acl["enabled"] is True
+        assert acl["channels"] == [b"*"]
+        assert acl["flags"] == ["on", "allchannels", "sanitize-payload"]
+        assert set(acl["keys"]) == {b"cache:*", b"objects:*"}
+        assert len(acl["passwords"]) == 2
+
+        # test removal of passwords
+        assert await r.acl_setuser(
+            username, enabled=True, reset=True, passwords=["+pass1", "+pass2"]
+        )
+        assert len((await r.acl_getuser(username))["passwords"]) == 2
+        assert await r.acl_setuser(username, enabled=True, passwords=["-pass2"])
+        assert len((await r.acl_getuser(username))["passwords"]) == 1
+
+        # Resets and tests that hashed passwords are set properly.
+        hashed_password = (
+            "5e884898da28047151d0e56f8dc629" "2773603d0d6aabbdd62a11ef721d1542d8"
+        )
+        assert await r.acl_setuser(
+            username, enabled=True, reset=True, hashed_passwords=["+" + hashed_password]
+        )
+        acl = await r.acl_getuser(username)
+        assert acl["passwords"] == [hashed_password]
+
+        # test removal of hashed passwords
+        assert await r.acl_setuser(
+            username,
+            enabled=True,
+            reset=True,
+            hashed_passwords=["+" + hashed_password],
+            passwords=["+pass1"],
+        )
+        assert len((await r.acl_getuser(username))["passwords"]) == 2
+        assert await r.acl_setuser(
+            username, enabled=True, hashed_passwords=["-" + hashed_password]
+        )
+        assert len((await r.acl_getuser(username))["passwords"]) == 1
+
+    @skip_if_server_version_lt(REDIS_6_VERSION)
+    async def test_acl_list(self, r: redis.Redis, request, event_loop):
+        username = "redis-py-user"
+
+        def teardown():
+            coro = r.acl_deluser(username)
+            if event_loop.is_running():
+                event_loop.create_task(coro)
+            else:
+                event_loop.run_until_complete(coro)
+
+        request.addfinalizer(teardown)
+
+        assert await r.acl_setuser(username, enabled=False, reset=True)
+        users = await r.acl_list()
+        assert f"user {username} off sanitize-payload &* -@all" in users
+
+    @skip_if_server_version_lt(REDIS_6_VERSION)
+    async def test_acl_log(self, r: redis.Redis, request, event_loop, create_redis):
+        username = "redis-py-user"
+
+        def teardown():
+            coro = r.acl_deluser(username)
+            if event_loop.is_running():
+                event_loop.create_task(coro)
+            else:
+                event_loop.run_until_complete(coro)
+
+        request.addfinalizer(teardown)
+        await r.acl_setuser(
+            username,
+            enabled=True,
+            reset=True,
+            commands=["+get", "+set", "+select"],
+            keys=["cache:*"],
+            nopass=True,
+        )
+        await r.acl_log_reset()
+
+        user_client = await create_redis(username=username)
+
+        # Valid operation and key
+        assert await user_client.set("cache:0", 1)
+        assert await user_client.get("cache:0") == b"1"
+
+        # Invalid key
+        with pytest.raises(exceptions.NoPermissionError):
+            await user_client.get("violated_cache:0")
+
+        # Invalid operation
+        with pytest.raises(exceptions.NoPermissionError):
+            await user_client.hset("cache:0", "hkey", "hval")
+
+        assert isinstance(await r.acl_log(), list)
+        assert len(await r.acl_log()) == 2
+        assert len(await r.acl_log(count=1)) == 1
+        assert isinstance((await r.acl_log())[0], dict)
+        assert "client-info" in (await r.acl_log(count=1))[0]
+        assert await r.acl_log_reset()
+
+    @skip_if_server_version_lt(REDIS_6_VERSION)
+    async def test_acl_setuser_categories_without_prefix_fails(
+        self, r: redis.Redis, request, event_loop
+    ):
+        username = "redis-py-user"
+
+        def teardown():
+            coro = r.acl_deluser(username)
+            if event_loop.is_running():
+                event_loop.create_task(coro)
+            else:
+                event_loop.run_until_complete(coro)
+
+        request.addfinalizer(teardown)
+
+        with pytest.raises(exceptions.DataError):
+            await r.acl_setuser(username, categories=["list"])
+
+    @skip_if_server_version_lt(REDIS_6_VERSION)
+    async def test_acl_setuser_commands_without_prefix_fails(
+        self, r: redis.Redis, request, event_loop
+    ):
+        username = "redis-py-user"
+
+        def teardown():
+            coro = r.acl_deluser(username)
+            if event_loop.is_running():
+                event_loop.create_task(coro)
+            else:
+                event_loop.run_until_complete(coro)
+
+        request.addfinalizer(teardown)
+
+        with pytest.raises(exceptions.DataError):
+            await r.acl_setuser(username, commands=["get"])
+
+    @skip_if_server_version_lt(REDIS_6_VERSION)
+    async def test_acl_setuser_add_passwords_and_nopass_fails(
+        self, r: redis.Redis, request, event_loop
+    ):
+        username = "redis-py-user"
+
+        def teardown():
+            coro = r.acl_deluser(username)
+            if event_loop.is_running():
+                event_loop.create_task(coro)
+            else:
+                event_loop.run_until_complete(coro)
+
+        request.addfinalizer(teardown)
+
+        with pytest.raises(exceptions.DataError):
+            await r.acl_setuser(username, passwords="+mypass", nopass=True)
+
+    @skip_if_server_version_lt(REDIS_6_VERSION)
+    async def test_acl_users(self, r: redis.Redis):
+        users = await r.acl_users()
+        assert isinstance(users, list)
+        assert len(users) > 0
+
+    @skip_if_server_version_lt(REDIS_6_VERSION)
+    async def test_acl_whoami(self, r: redis.Redis):
+        username = await r.acl_whoami()
+        assert isinstance(username, str)
+
+    async def test_client_list(self, r: redis.Redis):
+        clients = await r.client_list()
+        assert isinstance(clients[0], dict)
+        assert "addr" in clients[0]
+
+    @skip_if_server_version_lt("5.0.0")
+    async def test_client_list_type(self, r: redis.Redis):
+        with pytest.raises(exceptions.RedisError):
+            await r.client_list(_type="not a client type")
+        for client_type in ["normal", "master", "replica", "pubsub"]:
+            clients = await r.client_list(_type=client_type)
+            assert isinstance(clients, list)
+
+    @skip_if_server_version_lt("5.0.0")
+    async def test_client_id(self, r: redis.Redis):
+        assert await r.client_id() > 0
+
+    @skip_if_server_version_lt("5.0.0")
+    async def test_client_unblock(self, r: redis.Redis):
+        myid = await r.client_id()
+        assert not await r.client_unblock(myid)
+        assert not await r.client_unblock(myid, error=True)
+        assert not await r.client_unblock(myid, error=False)
+
+    @skip_if_server_version_lt("2.6.9")
+    async def test_client_getname(self, r: redis.Redis):
+        assert await r.client_getname() is None
+
+    @skip_if_server_version_lt("2.6.9")
+    async def test_client_setname(self, r: redis.Redis):
+        assert await r.client_setname("redis_py_test")
+        assert await r.client_getname() == "redis_py_test"
+
+    @skip_if_server_version_lt("2.6.9")
+    async def test_client_kill(self, r: redis.Redis, r2):
+        await r.client_setname("redis-py-c1")
+        await r2.client_setname("redis-py-c2")
+        clients = [
+            client
+            for client in await r.client_list()
+            if client.get("name") in ["redis-py-c1", "redis-py-c2"]
+        ]
+        assert len(clients) == 2
+
+        clients_by_name = {client.get("name"): client for client in clients}
+
+        client_addr = clients_by_name["redis-py-c2"].get("addr")
+        assert await r.client_kill(client_addr) is True
+
+        clients = [
+            client
+            for client in await r.client_list()
+            if client.get("name") in ["redis-py-c1", "redis-py-c2"]
+        ]
+        assert len(clients) == 1
+        assert clients[0].get("name") == "redis-py-c1"
+
+    @skip_if_server_version_lt("2.8.12")
+    async def test_client_kill_filter_invalid_params(self, r: redis.Redis):
+        # empty
+        with pytest.raises(exceptions.DataError):
+            await r.client_kill_filter()
+
+        # invalid skipme
+        with pytest.raises(exceptions.DataError):
+            await r.client_kill_filter(skipme="yeah")  # type: ignore
+
+        # invalid type
+        with pytest.raises(exceptions.DataError):
+            await r.client_kill_filter(_type="caster")  # type: ignore
+
+    @skip_if_server_version_lt("2.8.12")
+    async def test_client_kill_filter_by_id(self, r: redis.Redis, r2):
+        await r.client_setname("redis-py-c1")
+        await r2.client_setname("redis-py-c2")
+        clients = [
+            client
+            for client in await r.client_list()
+            if client.get("name") in ["redis-py-c1", "redis-py-c2"]
+        ]
+        assert len(clients) == 2
+
+        clients_by_name = {client.get("name"): client for client in clients}
+
+        client_2_id = clients_by_name["redis-py-c2"].get("id")
+        resp = await r.client_kill_filter(_id=client_2_id)
+        assert resp == 1
+
+        clients = [
+            client
+            for client in await r.client_list()
+            if client.get("name") in ["redis-py-c1", "redis-py-c2"]
+        ]
+        assert len(clients) == 1
+        assert clients[0].get("name") == "redis-py-c1"
+
+    @skip_if_server_version_lt("2.8.12")
+    async def test_client_kill_filter_by_addr(self, r: redis.Redis, r2):
+        await r.client_setname("redis-py-c1")
+        await r2.client_setname("redis-py-c2")
+        clients = [
+            client
+            for client in await r.client_list()
+            if client.get("name") in ["redis-py-c1", "redis-py-c2"]
+        ]
+        assert len(clients) == 2
+
+        clients_by_name = {client.get("name"): client for client in clients}
+
+        client_2_addr = clients_by_name["redis-py-c2"].get("addr")
+        resp = await r.client_kill_filter(addr=client_2_addr)
+        assert resp == 1
+
+        clients = [
+            client
+            for client in await r.client_list()
+            if client.get("name") in ["redis-py-c1", "redis-py-c2"]
+        ]
+        assert len(clients) == 1
+        assert clients[0].get("name") == "redis-py-c1"
+
+    @skip_if_server_version_lt("2.6.9")
+    async def test_client_list_after_client_setname(self, r: redis.Redis):
+        await r.client_setname("redis_py_test")
+        clients = await r.client_list()
+        # we don't know which client ours will be
+        assert "redis_py_test" in [c["name"] for c in clients]
+
+    @skip_if_server_version_lt("2.9.50")
+    async def test_client_pause(self, r: redis.Redis):
+        assert await r.client_pause(1)
+        assert await r.client_pause(timeout=1)
+        with pytest.raises(exceptions.RedisError):
+            await r.client_pause(timeout="not an integer")
+
+    async def test_config_get(self, r: redis.Redis):
+        data = await r.config_get()
+        assert "maxmemory" in data
+        assert data["maxmemory"].isdigit()
+
+    async def test_config_resetstat(self, r: redis.Redis):
+        await r.ping()
+        prior_commands_processed = int((await r.info())["total_commands_processed"])
+        assert prior_commands_processed >= 1
+        await r.config_resetstat()
+        reset_commands_processed = int((await r.info())["total_commands_processed"])
+        assert reset_commands_processed < prior_commands_processed
+
+    async def test_config_set(self, r: redis.Redis):
+        data = await r.config_get()
+        rdbname = data["dbfilename"]
+        try:
+            assert await r.config_set("dbfilename", "redis_py_test.rdb")
+            assert (await r.config_get())["dbfilename"] == "redis_py_test.rdb"
+        finally:
+            assert await r.config_set("dbfilename", rdbname)
+
+    async def test_dbsize(self, r: redis.Redis):
+        await r.set("a", "foo")
+        await r.set("b", "bar")
+        assert await r.dbsize() == 2
+
+    async def test_echo(self, r: redis.Redis):
+        assert await r.echo("foo bar") == b"foo bar"
+
+    async def test_info(self, r: redis.Redis):
+        await r.set("a", "foo")
+        await r.set("b", "bar")
+        info = await r.info()
+        assert isinstance(info, dict)
+        assert info["db9"]["keys"] == 2
+
+    async def test_lastsave(self, r: redis.Redis):
+        assert isinstance(await r.lastsave(), datetime.datetime)
+
+    async def test_object(self, r: redis.Redis):
+        await r.set("a", "foo")
+        assert isinstance(await r.object("refcount", "a"), int)
+        assert isinstance(await r.object("idletime", "a"), int)
+        assert await r.object("encoding", "a") in (b"raw", b"embstr")
+        assert await r.object("idletime", "invalid-key") is None
+
+    async def test_ping(self, r: redis.Redis):
+        assert await r.ping()
+
+    async def test_slowlog_get(self, r: redis.Redis, slowlog):
+        assert await r.slowlog_reset()
+        unicode_string = chr(3456) + "abcd" + chr(3421)
+        await r.get(unicode_string)
+        slowlog = await r.slowlog_get()
+        assert isinstance(slowlog, list)
+        commands = [log["command"] for log in slowlog]
+
+        get_command = b" ".join((b"GET", unicode_string.encode("utf-8")))
+        assert get_command in commands
+        assert b"SLOWLOG RESET" in commands
+        # the order should be ['GET <uni string>', 'SLOWLOG RESET'],
+        # but if other clients are executing commands at the same time, there
+        # could be commands, before, between, or after, so just check that
+        # the two we care about are in the appropriate order.
+        assert commands.index(get_command) < commands.index(b"SLOWLOG RESET")
+
+        # make sure other attributes are typed correctly
+        assert isinstance(slowlog[0]["start_time"], int)
+        assert isinstance(slowlog[0]["duration"], int)
+
+    async def test_slowlog_get_limit(self, r: redis.Redis, slowlog):
+        assert await r.slowlog_reset()
+        await r.get("foo")
+        slowlog = await r.slowlog_get(1)
+        assert isinstance(slowlog, list)
+        # only one command, based on the number we passed to slowlog_get()
+        assert len(slowlog) == 1
+
+    async def test_slowlog_length(self, r: redis.Redis, slowlog):
+        await r.get("foo")
+        assert isinstance(await r.slowlog_len(), int)
+
+    @skip_if_server_version_lt("2.6.0")
+    async def test_time(self, r: redis.Redis):
+        t = await r.time()
+        assert len(t) == 2
+        assert isinstance(t[0], int)
+        assert isinstance(t[1], int)
+
+    # BASIC KEY COMMANDS
+    async def test_append(self, r: redis.Redis):
+        assert await r.append("a", "a1") == 2
+        assert await r.get("a") == b"a1"
+        assert await r.append("a", "a2") == 4
+        assert await r.get("a") == b"a1a2"
+
+    @skip_if_server_version_lt("2.6.0")
+    async def test_bitcount(self, r: redis.Redis):
+        await r.setbit("a", 5, True)
+        assert await r.bitcount("a") == 1
+        await r.setbit("a", 6, True)
+        assert await r.bitcount("a") == 2
+        await r.setbit("a", 5, False)
+        assert await r.bitcount("a") == 1
+        await r.setbit("a", 9, True)
+        await r.setbit("a", 17, True)
+        await r.setbit("a", 25, True)
+        await r.setbit("a", 33, True)
+        assert await r.bitcount("a") == 5
+        assert await r.bitcount("a", 0, -1) == 5
+        assert await r.bitcount("a", 2, 3) == 2
+        assert await r.bitcount("a", 2, -1) == 3
+        assert await r.bitcount("a", -2, -1) == 2
+        assert await r.bitcount("a", 1, 1) == 1
+
+    @skip_if_server_version_lt("2.6.0")
+    async def test_bitop_not_empty_string(self, r: redis.Redis):
+        await r.set("a", "")
+        await r.bitop("not", "r", "a")
+        assert await r.get("r") is None
+
+    @skip_if_server_version_lt("2.6.0")
+    async def test_bitop_not(self, r: redis.Redis):
+        test_str = b"\xAA\x00\xFF\x55"
+        correct = ~0xAA00FF55 & 0xFFFFFFFF
+        await r.set("a", test_str)
+        await r.bitop("not", "r", "a")
+        assert int(binascii.hexlify(await r.get("r")), 16) == correct
+
+    @skip_if_server_version_lt("2.6.0")
+    async def test_bitop_not_in_place(self, r: redis.Redis):
+        test_str = b"\xAA\x00\xFF\x55"
+        correct = ~0xAA00FF55 & 0xFFFFFFFF
+        await r.set("a", test_str)
+        await r.bitop("not", "a", "a")
+        assert int(binascii.hexlify(await r.get("a")), 16) == correct
+
+    @skip_if_server_version_lt("2.6.0")
+    async def test_bitop_single_string(self, r: redis.Redis):
+        test_str = b"\x01\x02\xFF"
+        await r.set("a", test_str)
+        await r.bitop("and", "res1", "a")
+        await r.bitop("or", "res2", "a")
+        await r.bitop("xor", "res3", "a")
+        assert await r.get("res1") == test_str
+        assert await r.get("res2") == test_str
+        assert await r.get("res3") == test_str
+
+    @skip_if_server_version_lt("2.6.0")
+    async def test_bitop_string_operands(self, r: redis.Redis):
+        await r.set("a", b"\x01\x02\xFF\xFF")
+        await r.set("b", b"\x01\x02\xFF")
+        await r.bitop("and", "res1", "a", "b")
+        await r.bitop("or", "res2", "a", "b")
+        await r.bitop("xor", "res3", "a", "b")
+        assert int(binascii.hexlify(await r.get("res1")), 16) == 0x0102FF00
+        assert int(binascii.hexlify(await r.get("res2")), 16) == 0x0102FFFF
+        assert int(binascii.hexlify(await r.get("res3")), 16) == 0x000000FF
+
+    @skip_if_server_version_lt("2.8.7")
+    async def test_bitpos(self, r: redis.Redis):
+        key = "key:bitpos"
+        await r.set(key, b"\xff\xf0\x00")
+        assert await r.bitpos(key, 0) == 12
+        assert await r.bitpos(key, 0, 2, -1) == 16
+        assert await r.bitpos(key, 0, -2, -1) == 12
+        await r.set(key, b"\x00\xff\xf0")
+        assert await r.bitpos(key, 1, 0) == 8
+        assert await r.bitpos(key, 1, 1) == 8
+        await r.set(key, b"\x00\x00\x00")
+        assert await r.bitpos(key, 1) == -1
+
+    @skip_if_server_version_lt("2.8.7")
+    async def test_bitpos_wrong_arguments(self, r: redis.Redis):
+        key = "key:bitpos:wrong:args"
+        await r.set(key, b"\xff\xf0\x00")
+        with pytest.raises(exceptions.RedisError):
+            await r.bitpos(key, 0, end=1) == 12
+        with pytest.raises(exceptions.RedisError):
+            await r.bitpos(key, 7) == 12
+
+    async def test_decr(self, r: redis.Redis):
+        assert await r.decr("a") == -1
+        assert await r.get("a") == b"-1"
+        assert await r.decr("a") == -2
+        assert await r.get("a") == b"-2"
+        assert await r.decr("a", amount=5) == -7
+        assert await r.get("a") == b"-7"
+
+    async def test_decrby(self, r: redis.Redis):
+        assert await r.decrby("a", amount=2) == -2
+        assert await r.decrby("a", amount=3) == -5
+        assert await r.get("a") == b"-5"
+
+    async def test_delete(self, r: redis.Redis):
+        assert await r.delete("a") == 0
+        await r.set("a", "foo")
+        assert await r.delete("a") == 1
+
+    async def test_delete_with_multiple_keys(self, r: redis.Redis):
+        await r.set("a", "foo")
+        await r.set("b", "bar")
+        assert await r.delete("a", "b") == 2
+        assert await r.get("a") is None
+        assert await r.get("b") is None
+
+    async def test_delitem(self, r: redis.Redis):
+        await r.set("a", "foo")
+        await r.delete("a")
+        assert await r.get("a") is None
+
+    @skip_if_server_version_lt("4.0.0")
+    async def test_unlink(self, r: redis.Redis):
+        assert await r.unlink("a") == 0
+        await r.set("a", "foo")
+        assert await r.unlink("a") == 1
+        assert await r.get("a") is None
+
+    @skip_if_server_version_lt("4.0.0")
+    async def test_unlink_with_multiple_keys(self, r: redis.Redis):
+        await r.set("a", "foo")
+        await r.set("b", "bar")
+        assert await r.unlink("a", "b") == 2
+        assert await r.get("a") is None
+        assert await r.get("b") is None
+
+    @skip_if_server_version_lt("2.6.0")
+    async def test_dump_and_restore(self, r: redis.Redis):
+        await r.set("a", "foo")
+        dumped = await r.dump("a")
+        await r.delete("a")
+        await r.restore("a", 0, dumped)
+        assert await r.get("a") == b"foo"
+
+    @skip_if_server_version_lt("3.0.0")
+    async def test_dump_and_restore_and_replace(self, r: redis.Redis):
+        await r.set("a", "bar")
+        dumped = await r.dump("a")
+        with pytest.raises(redis.ResponseError):
+            await r.restore("a", 0, dumped)
+
+        await r.restore("a", 0, dumped, replace=True)
+        assert await r.get("a") == b"bar"
+
+    @skip_if_server_version_lt("5.0.0")
+    async def test_dump_and_restore_absttl(self, r: redis.Redis):
+        await r.set("a", "foo")
+        dumped = await r.dump("a")
+        await r.delete("a")
+        ttl = int(
+            (await redis_server_time(r) + datetime.timedelta(minutes=1)).timestamp()
+            * 1000
+        )
+        await r.restore("a", ttl, dumped, absttl=True)
+        assert await r.get("a") == b"foo"
+        assert 0 < await r.ttl("a") <= 61
+
+    async def test_exists(self, r: redis.Redis):
+        assert await r.exists("a") == 0
+        await r.set("a", "foo")
+        await r.set("b", "bar")
+        assert await r.exists("a") == 1
+        assert await r.exists("a", "b") == 2
+
+    async def test_exists_contains(self, r: redis.Redis):
+        assert not await r.exists("a")
+        await r.set("a", "foo")
+        assert await r.exists("a")
+
+    async def test_expire(self, r: redis.Redis):
+        assert not await r.expire("a", 10)
+        await r.set("a", "foo")
+        assert await r.expire("a", 10)
+        assert 0 < await r.ttl("a") <= 10
+        assert await r.persist("a")
+        assert await r.ttl("a") == -1
+
+    async def test_expireat_datetime(self, r: redis.Redis):
+        expire_at = await redis_server_time(r) + datetime.timedelta(minutes=1)
+        await r.set("a", "foo")
+        assert await r.expireat("a", expire_at)
+        assert 0 < await r.ttl("a") <= 61
+
+    async def test_expireat_no_key(self, r: redis.Redis):
+        expire_at = await redis_server_time(r) + datetime.timedelta(minutes=1)
+        assert not await r.expireat("a", expire_at)
+
+    async def test_expireat_unixtime(self, r: redis.Redis):
+        expire_at = await redis_server_time(r) + datetime.timedelta(minutes=1)
+        await r.set("a", "foo")
+        expire_at_seconds = int(time.mktime(expire_at.timetuple()))
+        assert await r.expireat("a", expire_at_seconds)
+        assert 0 < await r.ttl("a") <= 61
+
+    async def test_get_and_set(self, r: redis.Redis):
+        # get and set can't be tested independently of each other
+        assert await r.get("a") is None
+        byte_string = b"value"
+        integer = 5
+        unicode_string = chr(3456) + "abcd" + chr(3421)
+        assert await r.set("byte_string", byte_string)
+        assert await r.set("integer", 5)
+        assert await r.set("unicode_string", unicode_string)
+        assert await r.get("byte_string") == byte_string
+        assert await r.get("integer") == str(integer).encode()
+        assert (await r.get("unicode_string")).decode("utf-8") == unicode_string
+
+    async def test_get_set_bit(self, r: redis.Redis):
+        # no value
+        assert not await r.getbit("a", 5)
+        # set bit 5
+        assert not await r.setbit("a", 5, True)
+        assert await r.getbit("a", 5)
+        # unset bit 4
+        assert not await r.setbit("a", 4, False)
+        assert not await r.getbit("a", 4)
+        # set bit 4
+        assert not await r.setbit("a", 4, True)
+        assert await r.getbit("a", 4)
+        # set bit 5 again
+        assert await r.setbit("a", 5, True)
+        assert await r.getbit("a", 5)
+
+    async def test_getrange(self, r: redis.Redis):
+        await r.set("a", "foo")
+        assert await r.getrange("a", 0, 0) == b"f"
+        assert await r.getrange("a", 0, 2) == b"foo"
+        assert await r.getrange("a", 3, 4) == b""
+
+    async def test_getset(self, r: redis.Redis):
+        assert await r.getset("a", "foo") is None
+        assert await r.getset("a", "bar") == b"foo"
+        assert await r.get("a") == b"bar"
+
+    async def test_incr(self, r: redis.Redis):
+        assert await r.incr("a") == 1
+        assert await r.get("a") == b"1"
+        assert await r.incr("a") == 2
+        assert await r.get("a") == b"2"
+        assert await r.incr("a", amount=5) == 7
+        assert await r.get("a") == b"7"
+
+    async def test_incrby(self, r: redis.Redis):
+        assert await r.incrby("a") == 1
+        assert await r.incrby("a", 4) == 5
+        assert await r.get("a") == b"5"
+
+    @skip_if_server_version_lt("2.6.0")
+    async def test_incrbyfloat(self, r: redis.Redis):
+        assert await r.incrbyfloat("a") == 1.0
+        assert await r.get("a") == b"1"
+        assert await r.incrbyfloat("a", 1.1) == 2.1
+        assert float(await r.get("a")) == float(2.1)
+
+    async def test_keys(self, r: redis.Redis):
+        assert await r.keys() == []
+        keys_with_underscores = {b"test_a", b"test_b"}
+        keys = keys_with_underscores.union({b"testc"})
+        for key in keys:
+            await r.set(key, 1)
+        assert set(await r.keys(pattern="test_*")) == keys_with_underscores
+        assert set(await r.keys(pattern="test*")) == keys
+
+    async def test_mget(self, r: redis.Redis):
+        assert await r.mget([]) == []
+        assert await r.mget(["a", "b"]) == [None, None]
+        await r.set("a", "1")
+        await r.set("b", "2")
+        await r.set("c", "3")
+        assert await r.mget("a", "other", "b", "c") == [b"1", None, b"2", b"3"]
+
+    async def test_mset(self, r: redis.Redis):
+        d = {"a": b"1", "b": b"2", "c": b"3"}
+        assert await r.mset(d)
+        for k, v in d.items():
+            assert await r.get(k) == v
+
+    async def test_msetnx(self, r: redis.Redis):
+        d = {"a": b"1", "b": b"2", "c": b"3"}
+        assert await r.msetnx(d)
+        d2 = {"a": b"x", "d": b"4"}
+        assert not await r.msetnx(d2)
+        for k, v in d.items():
+            assert await r.get(k) == v
+        assert await r.get("d") is None
+
+    @skip_if_server_version_lt("2.6.0")
+    async def test_pexpire(self, r: redis.Redis):
+        assert not await r.pexpire("a", 60000)
+        await r.set("a", "foo")
+        assert await r.pexpire("a", 60000)
+        assert 0 < await r.pttl("a") <= 60000
+        assert await r.persist("a")
+        assert await r.pttl("a") == -1
+
+    @skip_if_server_version_lt("2.6.0")
+    async def test_pexpireat_datetime(self, r: redis.Redis):
+        expire_at = await redis_server_time(r) + datetime.timedelta(minutes=1)
+        await r.set("a", "foo")
+        assert await r.pexpireat("a", expire_at)
+        assert 0 < await r.pttl("a") <= 61000
+
+    @skip_if_server_version_lt("2.6.0")
+    async def test_pexpireat_no_key(self, r: redis.Redis):
+        expire_at = await redis_server_time(r) + datetime.timedelta(minutes=1)
+        assert not await r.pexpireat("a", expire_at)
+
+    @skip_if_server_version_lt("2.6.0")
+    async def test_pexpireat_unixtime(self, r: redis.Redis):
+        expire_at = await redis_server_time(r) + datetime.timedelta(minutes=1)
+        await r.set("a", "foo")
+        expire_at_seconds = int(time.mktime(expire_at.timetuple())) * 1000
+        assert await r.pexpireat("a", expire_at_seconds)
+        assert 0 < await r.pttl("a") <= 61000
+
+    @skip_if_server_version_lt("2.6.0")
+    async def test_psetex(self, r: redis.Redis):
+        assert await r.psetex("a", 1000, "value")
+        assert await r.get("a") == b"value"
+        assert 0 < await r.pttl("a") <= 1000
+
+    @skip_if_server_version_lt("2.6.0")
+    async def test_psetex_timedelta(self, r: redis.Redis):
+        expire_at = datetime.timedelta(milliseconds=1000)
+        assert await r.psetex("a", expire_at, "value")
+        assert await r.get("a") == b"value"
+        assert 0 < await r.pttl("a") <= 1000
+
+    @skip_if_server_version_lt("2.6.0")
+    async def test_pttl(self, r: redis.Redis):
+        assert not await r.pexpire("a", 10000)
+        await r.set("a", "1")
+        assert await r.pexpire("a", 10000)
+        assert 0 < await r.pttl("a") <= 10000
+        assert await r.persist("a")
+        assert await r.pttl("a") == -1
+
+    @skip_if_server_version_lt("2.8.0")
+    async def test_pttl_no_key(self, r: redis.Redis):
+        """PTTL on servers 2.8 and after return -2 when the key doesn't exist"""
+        assert await r.pttl("a") == -2
+
+    async def test_randomkey(self, r: redis.Redis):
+        assert await r.randomkey() is None
+        for key in ("a", "b", "c"):
+            await r.set(key, 1)
+        assert await r.randomkey() in (b"a", b"b", b"c")
+
+    async def test_rename(self, r: redis.Redis):
+        await r.set("a", "1")
+        assert await r.rename("a", "b")
+        assert await r.get("a") is None
+        assert await r.get("b") == b"1"
+
+    async def test_renamenx(self, r: redis.Redis):
+        await r.set("a", "1")
+        await r.set("b", "2")
+        assert not await r.renamenx("a", "b")
+        assert await r.get("a") == b"1"
+        assert await r.get("b") == b"2"
+
+    @skip_if_server_version_lt("2.6.0")
+    async def test_set_nx(self, r: redis.Redis):
+        assert await r.set("a", "1", nx=True)
+        assert not await r.set("a", "2", nx=True)
+        assert await r.get("a") == b"1"
+
+    @skip_if_server_version_lt("2.6.0")
+    async def test_set_xx(self, r: redis.Redis):
+        assert not await r.set("a", "1", xx=True)
+        assert await r.get("a") is None
+        await r.set("a", "bar")
+        assert await r.set("a", "2", xx=True)
+        assert await r.get("a") == b"2"
+
+    @skip_if_server_version_lt("2.6.0")
+    async def test_set_px(self, r: redis.Redis):
+        assert await r.set("a", "1", px=10000)
+        assert await r.get("a") == b"1"
+        assert 0 < await r.pttl("a") <= 10000
+        assert 0 < await r.ttl("a") <= 10
+
+    @skip_if_server_version_lt("2.6.0")
+    async def test_set_px_timedelta(self, r: redis.Redis):
+        expire_at = datetime.timedelta(milliseconds=1000)
+        assert await r.set("a", "1", px=expire_at)
+        assert 0 < await r.pttl("a") <= 1000
+        assert 0 < await r.ttl("a") <= 1
+
+    @skip_if_server_version_lt("2.6.0")
+    async def test_set_ex(self, r: redis.Redis):
+        assert await r.set("a", "1", ex=10)
+        assert 0 < await r.ttl("a") <= 10
+
+    @skip_if_server_version_lt("2.6.0")
+    async def test_set_ex_timedelta(self, r: redis.Redis):
+        expire_at = datetime.timedelta(seconds=60)
+        assert await r.set("a", "1", ex=expire_at)
+        assert 0 < await r.ttl("a") <= 60
+
+    @skip_if_server_version_lt("2.6.0")
+    async def test_set_multipleoptions(self, r: redis.Redis):
+        await r.set("a", "val")
+        assert await r.set("a", "1", xx=True, px=10000)
+        assert 0 < await r.ttl("a") <= 10
+
+    @skip_if_server_version_lt(REDIS_6_VERSION)
+    async def test_set_keepttl(self, r: redis.Redis):
+        await r.set("a", "val")
+        assert await r.set("a", "1", xx=True, px=10000)
+        assert 0 < await r.ttl("a") <= 10
+        await r.set("a", "2", keepttl=True)
+        assert await r.get("a") == b"2"
+        assert 0 < await r.ttl("a") <= 10
+
+    async def test_setex(self, r: redis.Redis):
+        assert await r.setex("a", 60, "1")
+        assert await r.get("a") == b"1"
+        assert 0 < await r.ttl("a") <= 60
+
+    async def test_setnx(self, r: redis.Redis):
+        assert await r.setnx("a", "1")
+        assert await r.get("a") == b"1"
+        assert not await r.setnx("a", "2")
+        assert await r.get("a") == b"1"
+
+    async def test_setrange(self, r: redis.Redis):
+        assert await r.setrange("a", 5, "foo") == 8
+        assert await r.get("a") == b"\0\0\0\0\0foo"
+        await r.set("a", "abcdefghijh")
+        assert await r.setrange("a", 6, "12345") == 11
+        assert await r.get("a") == b"abcdef12345"
+
+    async def test_strlen(self, r: redis.Redis):
+        await r.set("a", "foo")
+        assert await r.strlen("a") == 3
+
+    async def test_substr(self, r: redis.Redis):
+        await r.set("a", "0123456789")
+        assert await r.substr("a", 0) == b"0123456789"
+        assert await r.substr("a", 2) == b"23456789"
+        assert await r.substr("a", 3, 5) == b"345"
+        assert await r.substr("a", 3, -2) == b"345678"
+
+    async def test_ttl(self, r: redis.Redis):
+        await r.set("a", "1")
+        assert await r.expire("a", 10)
+        assert 0 < await r.ttl("a") <= 10
+        assert await r.persist("a")
+        assert await r.ttl("a") == -1
+
+    @skip_if_server_version_lt("2.8.0")
+    async def test_ttl_nokey(self, r: redis.Redis):
+        """TTL on servers 2.8 and after return -2 when the key doesn't exist"""
+        assert await r.ttl("a") == -2
+
+    async def test_type(self, r: redis.Redis):
+        assert await r.type("a") == b"none"
+        await r.set("a", "1")
+        assert await r.type("a") == b"string"
+        await r.delete("a")
+        await r.lpush("a", "1")
+        assert await r.type("a") == b"list"
+        await r.delete("a")
+        await r.sadd("a", "1")
+        assert await r.type("a") == b"set"
+        await r.delete("a")
+        await r.zadd("a", {"1": 1})
+        assert await r.type("a") == b"zset"
+
+    # LIST COMMANDS
+    async def test_blpop(self, r: redis.Redis):
+        await r.rpush("a", "1", "2")
+        await r.rpush("b", "3", "4")
+        assert await r.blpop(["b", "a"], timeout=1) == (b"b", b"3")
+        assert await r.blpop(["b", "a"], timeout=1) == (b"b", b"4")
+        assert await r.blpop(["b", "a"], timeout=1) == (b"a", b"1")
+        assert await r.blpop(["b", "a"], timeout=1) == (b"a", b"2")
+        assert await r.blpop(["b", "a"], timeout=1) is None
+        await r.rpush("c", "1")
+        assert await r.blpop("c", timeout=1) == (b"c", b"1")
+
+    async def test_brpop(self, r: redis.Redis):
+        await r.rpush("a", "1", "2")
+        await r.rpush("b", "3", "4")
+        assert await r.brpop(["b", "a"], timeout=1) == (b"b", b"4")
+        assert await r.brpop(["b", "a"], timeout=1) == (b"b", b"3")
+        assert await r.brpop(["b", "a"], timeout=1) == (b"a", b"2")
+        assert await r.brpop(["b", "a"], timeout=1) == (b"a", b"1")
+        assert await r.brpop(["b", "a"], timeout=1) is None
+        await r.rpush("c", "1")
+        assert await r.brpop("c", timeout=1) == (b"c", b"1")
+
+    async def test_brpoplpush(self, r: redis.Redis):
+        await r.rpush("a", "1", "2")
+        await r.rpush("b", "3", "4")
+        assert await r.brpoplpush("a", "b") == b"2"
+        assert await r.brpoplpush("a", "b") == b"1"
+        assert await r.brpoplpush("a", "b", timeout=1) is None
+        assert await r.lrange("a", 0, -1) == []
+        assert await r.lrange("b", 0, -1) == [b"1", b"2", b"3", b"4"]
+
+    async def test_brpoplpush_empty_string(self, r: redis.Redis):
+        await r.rpush("a", "")
+        assert await r.brpoplpush("a", "b") == b""
+
+    async def test_lindex(self, r: redis.Redis):
+        await r.rpush("a", "1", "2", "3")
+        assert await r.lindex("a", "0") == b"1"
+        assert await r.lindex("a", "1") == b"2"
+        assert await r.lindex("a", "2") == b"3"
+
+    async def test_linsert(self, r: redis.Redis):
+        await r.rpush("a", "1", "2", "3")
+        assert await r.linsert("a", "after", "2", "2.5") == 4
+        assert await r.lrange("a", 0, -1) == [b"1", b"2", b"2.5", b"3"]
+        assert await r.linsert("a", "before", "2", "1.5") == 5
+        assert await r.lrange("a", 0, -1) == [b"1", b"1.5", b"2", b"2.5", b"3"]
+
+    async def test_llen(self, r: redis.Redis):
+        await r.rpush("a", "1", "2", "3")
+        assert await r.llen("a") == 3
+
+    async def test_lpop(self, r: redis.Redis):
+        await r.rpush("a", "1", "2", "3")
+        assert await r.lpop("a") == b"1"
+        assert await r.lpop("a") == b"2"
+        assert await r.lpop("a") == b"3"
+        assert await r.lpop("a") is None
+
+    async def test_lpush(self, r: redis.Redis):
+        assert await r.lpush("a", "1") == 1
+        assert await r.lpush("a", "2") == 2
+        assert await r.lpush("a", "3", "4") == 4
+        assert await r.lrange("a", 0, -1) == [b"4", b"3", b"2", b"1"]
+
+    async def test_lpushx(self, r: redis.Redis):
+        assert await r.lpushx("a", "1") == 0
+        assert await r.lrange("a", 0, -1) == []
+        await r.rpush("a", "1", "2", "3")
+        assert await r.lpushx("a", "4") == 4
+        assert await r.lrange("a", 0, -1) == [b"4", b"1", b"2", b"3"]
+
+    async def test_lrange(self, r: redis.Redis):
+        await r.rpush("a", "1", "2", "3", "4", "5")
+        assert await r.lrange("a", 0, 2) == [b"1", b"2", b"3"]
+        assert await r.lrange("a", 2, 10) == [b"3", b"4", b"5"]
+        assert await r.lrange("a", 0, -1) == [b"1", b"2", b"3", b"4", b"5"]
+
+    async def test_lrem(self, r: redis.Redis):
+        await r.rpush("a", "Z", "b", "Z", "Z", "c", "Z", "Z")
+        # remove the first 'Z'  item
+        assert await r.lrem("a", 1, "Z") == 1
+        assert await r.lrange("a", 0, -1) == [b"b", b"Z", b"Z", b"c", b"Z", b"Z"]
+        # remove the last 2 'Z' items
+        assert await r.lrem("a", -2, "Z") == 2
+        assert await r.lrange("a", 0, -1) == [b"b", b"Z", b"Z", b"c"]
+        # remove all 'Z' items
+        assert await r.lrem("a", 0, "Z") == 2
+        assert await r.lrange("a", 0, -1) == [b"b", b"c"]
+
+    async def test_lset(self, r: redis.Redis):
+        await r.rpush("a", "1", "2", "3")
+        assert await r.lrange("a", 0, -1) == [b"1", b"2", b"3"]
+        assert await r.lset("a", 1, "4")
+        assert await r.lrange("a", 0, 2) == [b"1", b"4", b"3"]
+
+    async def test_ltrim(self, r: redis.Redis):
+        await r.rpush("a", "1", "2", "3")
+        assert await r.ltrim("a", 0, 1)
+        assert await r.lrange("a", 0, -1) == [b"1", b"2"]
+
+    async def test_rpop(self, r: redis.Redis):
+        await r.rpush("a", "1", "2", "3")
+        assert await r.rpop("a") == b"3"
+        assert await r.rpop("a") == b"2"
+        assert await r.rpop("a") == b"1"
+        assert await r.rpop("a") is None
+
+    async def test_rpoplpush(self, r: redis.Redis):
+        await r.rpush("a", "a1", "a2", "a3")
+        await r.rpush("b", "b1", "b2", "b3")
+        assert await r.rpoplpush("a", "b") == b"a3"
+        assert await r.lrange("a", 0, -1) == [b"a1", b"a2"]
+        assert await r.lrange("b", 0, -1) == [b"a3", b"b1", b"b2", b"b3"]
+
+    async def test_rpush(self, r: redis.Redis):
+        assert await r.rpush("a", "1") == 1
+        assert await r.rpush("a", "2") == 2
+        assert await r.rpush("a", "3", "4") == 4
+        assert await r.lrange("a", 0, -1) == [b"1", b"2", b"3", b"4"]
+
+    @skip_if_server_version_lt("6.0.6")
+    async def test_lpos(self, r: redis.Redis):
+        assert await r.rpush("a", "a", "b", "c", "1", "2", "3", "c", "c") == 8
+        assert await r.lpos("a", "a") == 0
+        assert await r.lpos("a", "c") == 2
+
+        assert await r.lpos("a", "c", rank=1) == 2
+        assert await r.lpos("a", "c", rank=2) == 6
+        assert await r.lpos("a", "c", rank=4) is None
+        assert await r.lpos("a", "c", rank=-1) == 7
+        assert await r.lpos("a", "c", rank=-2) == 6
+
+        assert await r.lpos("a", "c", count=0) == [2, 6, 7]
+        assert await r.lpos("a", "c", count=1) == [2]
+        assert await r.lpos("a", "c", count=2) == [2, 6]
+        assert await r.lpos("a", "c", count=100) == [2, 6, 7]
+
+        assert await r.lpos("a", "c", count=0, rank=2) == [6, 7]
+        assert await r.lpos("a", "c", count=2, rank=-1) == [7, 6]
+
+        assert await r.lpos("axxx", "c", count=0, rank=2) == []
+        assert await r.lpos("axxx", "c") is None
+
+        assert await r.lpos("a", "x", count=2) == []
+        assert await r.lpos("a", "x") is None
+
+        assert await r.lpos("a", "a", count=0, maxlen=1) == [0]
+        assert await r.lpos("a", "c", count=0, maxlen=1) == []
+        assert await r.lpos("a", "c", count=0, maxlen=3) == [2]
+        assert await r.lpos("a", "c", count=0, maxlen=3, rank=-1) == [7, 6]
+        assert await r.lpos("a", "c", count=0, maxlen=7, rank=2) == [6]
+
+    async def test_rpushx(self, r: redis.Redis):
+        assert await r.rpushx("a", "b") == 0
+        assert await r.lrange("a", 0, -1) == []
+        await r.rpush("a", "1", "2", "3")
+        assert await r.rpushx("a", "4") == 4
+        assert await r.lrange("a", 0, -1) == [b"1", b"2", b"3", b"4"]
+
+    # SCAN COMMANDS
+    @skip_if_server_version_lt("2.8.0")
+    async def test_scan(self, r: redis.Redis):
+        await r.set("a", 1)
+        await r.set("b", 2)
+        await r.set("c", 3)
+        cursor, keys = await r.scan()
+        assert cursor == 0
+        assert set(keys) == {b"a", b"b", b"c"}
+        _, keys = await r.scan(match="a")
+        assert set(keys) == {b"a"}
+
+    @skip_if_server_version_lt(REDIS_6_VERSION)
+    async def test_scan_type(self, r: redis.Redis):
+        await r.sadd("a-set", 1)
+        await r.hset("a-hash", "foo", 2)
+        await r.lpush("a-list", "aux", 3)
+        _, keys = await r.scan(match="a*", _type="SET")
+        assert set(keys) == {b"a-set"}
+
+    @skip_if_server_version_lt("2.8.0")
+    async def test_scan_iter(self, r: redis.Redis):
+        await r.set("a", 1)
+        await r.set("b", 2)
+        await r.set("c", 3)
+        keys = [k async for k in r.scan_iter()]
+        assert set(keys) == {b"a", b"b", b"c"}
+        keys = [k async for k in r.scan_iter(match="a")]
+        assert set(keys) == {b"a"}
+
+    @skip_if_server_version_lt("2.8.0")
+    async def test_sscan(self, r: redis.Redis):
+        await r.sadd("a", 1, 2, 3)
+        cursor, members = await r.sscan("a")
+        assert cursor == 0
+        assert set(members) == {b"1", b"2", b"3"}
+        _, members = await r.sscan("a", match=b"1")
+        assert set(members) == {b"1"}
+
+    @skip_if_server_version_lt("2.8.0")
+    async def test_sscan_iter(self, r: redis.Redis):
+        await r.sadd("a", 1, 2, 3)
+        members = [k async for k in r.sscan_iter("a")]
+        assert set(members) == {b"1", b"2", b"3"}
+        members = [k async for k in r.sscan_iter("a", match=b"1")]
+        assert set(members) == {b"1"}
+
+    @skip_if_server_version_lt("2.8.0")
+    async def test_hscan(self, r: redis.Redis):
+        await r.hset("a", mapping={"a": 1, "b": 2, "c": 3})
+        cursor, dic = await r.hscan("a")
+        assert cursor == 0
+        assert dic == {b"a": b"1", b"b": b"2", b"c": b"3"}
+        _, dic = await r.hscan("a", match="a")
+        assert dic == {b"a": b"1"}
+
+    @skip_if_server_version_lt("2.8.0")
+    async def test_hscan_iter(self, r: redis.Redis):
+        await r.hset("a", mapping={"a": 1, "b": 2, "c": 3})
+        dic = {k: v async for k, v in r.hscan_iter("a")}
+        assert dic == {b"a": b"1", b"b": b"2", b"c": b"3"}
+        dic = {k: v async for k, v in r.hscan_iter("a", match="a")}
+        assert dic == {b"a": b"1"}
+
+    @skip_if_server_version_lt("2.8.0")
+    async def test_zscan(self, r: redis.Redis):
+        await r.zadd("a", {"a": 1, "b": 2, "c": 3})
+        cursor, pairs = await r.zscan("a")
+        assert cursor == 0
+        assert set(pairs) == {(b"a", 1), (b"b", 2), (b"c", 3)}
+        _, pairs = await r.zscan("a", match="a")
+        assert set(pairs) == {(b"a", 1)}
+
+    @skip_if_server_version_lt("2.8.0")
+    async def test_zscan_iter(self, r: redis.Redis):
+        await r.zadd("a", {"a": 1, "b": 2, "c": 3})
+        pairs = [k async for k in r.zscan_iter("a")]
+        assert set(pairs) == {(b"a", 1), (b"b", 2), (b"c", 3)}
+        pairs = [k async for k in r.zscan_iter("a", match="a")]
+        assert set(pairs) == {(b"a", 1)}
+
+    # SET COMMANDS
+    async def test_sadd(self, r: redis.Redis):
+        members = {b"1", b"2", b"3"}
+        await r.sadd("a", *members)
+        assert await r.smembers("a") == members
+
+    async def test_scard(self, r: redis.Redis):
+        await r.sadd("a", "1", "2", "3")
+        assert await r.scard("a") == 3
+
+    async def test_sdiff(self, r: redis.Redis):
+        await r.sadd("a", "1", "2", "3")
+        assert await r.sdiff("a", "b") == {b"1", b"2", b"3"}
+        await r.sadd("b", "2", "3")
+        assert await r.sdiff("a", "b") == {b"1"}
+
+    async def test_sdiffstore(self, r: redis.Redis):
+        await r.sadd("a", "1", "2", "3")
+        assert await r.sdiffstore("c", "a", "b") == 3
+        assert await r.smembers("c") == {b"1", b"2", b"3"}
+        await r.sadd("b", "2", "3")
+        assert await r.sdiffstore("c", "a", "b") == 1
+        assert await r.smembers("c") == {b"1"}
+
+    async def test_sinter(self, r: redis.Redis):
+        await r.sadd("a", "1", "2", "3")
+        assert await r.sinter("a", "b") == set()
+        await r.sadd("b", "2", "3")
+        assert await r.sinter("a", "b") == {b"2", b"3"}
+
+    async def test_sinterstore(self, r: redis.Redis):
+        await r.sadd("a", "1", "2", "3")
+        assert await r.sinterstore("c", "a", "b") == 0
+        assert await r.smembers("c") == set()
+        await r.sadd("b", "2", "3")
+        assert await r.sinterstore("c", "a", "b") == 2
+        assert await r.smembers("c") == {b"2", b"3"}
+
+    async def test_sismember(self, r: redis.Redis):
+        await r.sadd("a", "1", "2", "3")
+        assert await r.sismember("a", "1")
+        assert await r.sismember("a", "2")
+        assert await r.sismember("a", "3")
+        assert not await r.sismember("a", "4")
+
+    async def test_smembers(self, r: redis.Redis):
+        await r.sadd("a", "1", "2", "3")
+        assert await r.smembers("a") == {b"1", b"2", b"3"}
+
+    async def test_smove(self, r: redis.Redis):
+        await r.sadd("a", "a1", "a2")
+        await r.sadd("b", "b1", "b2")
+        assert await r.smove("a", "b", "a1")
+        assert await r.smembers("a") == {b"a2"}
+        assert await r.smembers("b") == {b"b1", b"b2", b"a1"}
+
+    async def test_spop(self, r: redis.Redis):
+        s = [b"1", b"2", b"3"]
+        await r.sadd("a", *s)
+        value = await r.spop("a")
+        assert value in s
+        assert await r.smembers("a") == set(s) - {value}
+
+    @skip_if_server_version_lt("3.2.0")
+    async def test_spop_multi_value(self, r: redis.Redis):
+        s = [b"1", b"2", b"3"]
+        await r.sadd("a", *s)
+        values = await r.spop("a", 2)
+        assert len(values) == 2
+
+        for value in values:
+            assert value in s
+
+        assert await r.spop("a", 1) == list(set(s) - set(values))
+
+    async def test_srandmember(self, r: redis.Redis):
+        s = [b"1", b"2", b"3"]
+        await r.sadd("a", *s)
+        assert await r.srandmember("a") in s
+
+    @skip_if_server_version_lt("2.6.0")
+    async def test_srandmember_multi_value(self, r: redis.Redis):
+        s = [b"1", b"2", b"3"]
+        await r.sadd("a", *s)
+        randoms = await r.srandmember("a", number=2)
+        assert len(randoms) == 2
+        assert set(randoms).intersection(s) == set(randoms)
+
+    async def test_srem(self, r: redis.Redis):
+        await r.sadd("a", "1", "2", "3", "4")
+        assert await r.srem("a", "5") == 0
+        assert await r.srem("a", "2", "4") == 2
+        assert await r.smembers("a") == {b"1", b"3"}
+
+    async def test_sunion(self, r: redis.Redis):
+        await r.sadd("a", "1", "2")
+        await r.sadd("b", "2", "3")
+        assert await r.sunion("a", "b") == {b"1", b"2", b"3"}
+
+    async def test_sunionstore(self, r: redis.Redis):
+        await r.sadd("a", "1", "2")
+        await r.sadd("b", "2", "3")
+        assert await r.sunionstore("c", "a", "b") == 3
+        assert await r.smembers("c") == {b"1", b"2", b"3"}
+
+    # SORTED SET COMMANDS
+    async def test_zadd(self, r: redis.Redis):
+        mapping = {"a1": 1.0, "a2": 2.0, "a3": 3.0}
+        await r.zadd("a", mapping)
+        assert await r.zrange("a", 0, -1, withscores=True) == [
+            (b"a1", 1.0),
+            (b"a2", 2.0),
+            (b"a3", 3.0),
+        ]
+
+        # error cases
+        with pytest.raises(exceptions.DataError):
+            await r.zadd("a", {})
+
+        # cannot use both nx and xx options
+        with pytest.raises(exceptions.DataError):
+            await r.zadd("a", mapping, nx=True, xx=True)
+
+        # cannot use the incr options with more than one value
+        with pytest.raises(exceptions.DataError):
+            await r.zadd("a", mapping, incr=True)
+
+    async def test_zadd_nx(self, r: redis.Redis):
+        assert await r.zadd("a", {"a1": 1}) == 1
+        assert await r.zadd("a", {"a1": 99, "a2": 2}, nx=True) == 1
+        assert await r.zrange("a", 0, -1, withscores=True) == [
+            (b"a1", 1.0),
+            (b"a2", 2.0),
+        ]
+
+    async def test_zadd_xx(self, r: redis.Redis):
+        assert await r.zadd("a", {"a1": 1}) == 1
+        assert await r.zadd("a", {"a1": 99, "a2": 2}, xx=True) == 0
+        assert await r.zrange("a", 0, -1, withscores=True) == [(b"a1", 99.0)]
+
+    async def test_zadd_ch(self, r: redis.Redis):
+        assert await r.zadd("a", {"a1": 1}) == 1
+        assert await r.zadd("a", {"a1": 99, "a2": 2}, ch=True) == 2
+        assert await r.zrange("a", 0, -1, withscores=True) == [
+            (b"a2", 2.0),
+            (b"a1", 99.0),
+        ]
+
+    async def test_zadd_incr(self, r: redis.Redis):
+        assert await r.zadd("a", {"a1": 1}) == 1
+        assert await r.zadd("a", {"a1": 4.5}, incr=True) == 5.5
+
+    async def test_zadd_incr_with_xx(self, r: redis.Redis):
+        # this asks zadd to incr 'a1' only if it exists, but it clearly
+        # doesn't. Redis returns a null value in this case and so should
+        # redis-py
+        assert await r.zadd("a", {"a1": 1}, xx=True, incr=True) is None
+
+    async def test_zcard(self, r: redis.Redis):
+        await r.zadd("a", {"a1": 1, "a2": 2, "a3": 3})
+        assert await r.zcard("a") == 3
+
+    async def test_zcount(self, r: redis.Redis):
+        await r.zadd("a", {"a1": 1, "a2": 2, "a3": 3})
+        assert await r.zcount("a", "-inf", "+inf") == 3
+        assert await r.zcount("a", 1, 2) == 2
+        assert await r.zcount("a", "(" + str(1), 2) == 1
+        assert await r.zcount("a", 1, "(" + str(2)) == 1
+        assert await r.zcount("a", 10, 20) == 0
+
+    async def test_zincrby(self, r: redis.Redis):
+        await r.zadd("a", {"a1": 1, "a2": 2, "a3": 3})
+        assert await r.zincrby("a", 1, "a2") == 3.0
+        assert await r.zincrby("a", 5, "a3") == 8.0
+        assert await r.zscore("a", "a2") == 3.0
+        assert await r.zscore("a", "a3") == 8.0
+
+    @skip_if_server_version_lt("2.8.9")
+    async def test_zlexcount(self, r: redis.Redis):
+        await r.zadd("a", {"a": 0, "b": 0, "c": 0, "d": 0, "e": 0, "f": 0, "g": 0})
+        assert await r.zlexcount("a", "-", "+") == 7
+        assert await r.zlexcount("a", "[b", "[f") == 5
+
+    async def test_zinterstore_sum(self, r: redis.Redis):
+        await r.zadd("a", {"a1": 1, "a2": 1, "a3": 1})
+        await r.zadd("b", {"a1": 2, "a2": 2, "a3": 2})
+        await r.zadd("c", {"a1": 6, "a3": 5, "a4": 4})
+        assert await r.zinterstore("d", ["a", "b", "c"]) == 2
+        assert await r.zrange("d", 0, -1, withscores=True) == [(b"a3", 8), (b"a1", 9)]
+
+    async def test_zinterstore_max(self, r: redis.Redis):
+        await r.zadd("a", {"a1": 1, "a2": 1, "a3": 1})
+        await r.zadd("b", {"a1": 2, "a2": 2, "a3": 2})
+        await r.zadd("c", {"a1": 6, "a3": 5, "a4": 4})
+        assert await r.zinterstore("d", ["a", "b", "c"], aggregate="MAX") == 2
+        assert await r.zrange("d", 0, -1, withscores=True) == [(b"a3", 5), (b"a1", 6)]
+
+    async def test_zinterstore_min(self, r: redis.Redis):
+        await r.zadd("a", {"a1": 1, "a2": 2, "a3": 3})
+        await r.zadd("b", {"a1": 2, "a2": 3, "a3": 5})
+        await r.zadd("c", {"a1": 6, "a3": 5, "a4": 4})
+        assert await r.zinterstore("d", ["a", "b", "c"], aggregate="MIN") == 2
+        assert await r.zrange("d", 0, -1, withscores=True) == [(b"a1", 1), (b"a3", 3)]
+
+    async def test_zinterstore_with_weight(self, r: redis.Redis):
+        await r.zadd("a", {"a1": 1, "a2": 1, "a3": 1})
+        await r.zadd("b", {"a1": 2, "a2": 2, "a3": 2})
+        await r.zadd("c", {"a1": 6, "a3": 5, "a4": 4})
+        assert await r.zinterstore("d", {"a": 1, "b": 2, "c": 3}) == 2
+        assert await r.zrange("d", 0, -1, withscores=True) == [(b"a3", 20), (b"a1", 23)]
+
+    @skip_if_server_version_lt("4.9.0")
+    async def test_zpopmax(self, r: redis.Redis):
+        await r.zadd("a", {"a1": 1, "a2": 2, "a3": 3})
+        assert await r.zpopmax("a") == [(b"a3", 3)]
+
+        # with count
+        assert await r.zpopmax("a", count=2) == [(b"a2", 2), (b"a1", 1)]
+
+    @skip_if_server_version_lt("4.9.0")
+    async def test_zpopmin(self, r: redis.Redis):
+        await r.zadd("a", {"a1": 1, "a2": 2, "a3": 3})
+        assert await r.zpopmin("a") == [(b"a1", 1)]
+
+        # with count
+        assert await r.zpopmin("a", count=2) == [(b"a2", 2), (b"a3", 3)]
+
+    @skip_if_server_version_lt("4.9.0")
+    async def test_bzpopmax(self, r: redis.Redis):
+        await r.zadd("a", {"a1": 1, "a2": 2})
+        await r.zadd("b", {"b1": 10, "b2": 20})
+        assert await r.bzpopmax(["b", "a"], timeout=1) == (b"b", b"b2", 20)
+        assert await r.bzpopmax(["b", "a"], timeout=1) == (b"b", b"b1", 10)
+        assert await r.bzpopmax(["b", "a"], timeout=1) == (b"a", b"a2", 2)
+        assert await r.bzpopmax(["b", "a"], timeout=1) == (b"a", b"a1", 1)
+        assert await r.bzpopmax(["b", "a"], timeout=1) is None
+        await r.zadd("c", {"c1": 100})
+        assert await r.bzpopmax("c", timeout=1) == (b"c", b"c1", 100)
+
+    @skip_if_server_version_lt("4.9.0")
+    async def test_bzpopmin(self, r: redis.Redis):
+        await r.zadd("a", {"a1": 1, "a2": 2})
+        await r.zadd("b", {"b1": 10, "b2": 20})
+        assert await r.bzpopmin(["b", "a"], timeout=1) == (b"b", b"b1", 10)
+        assert await r.bzpopmin(["b", "a"], timeout=1) == (b"b", b"b2", 20)
+        assert await r.bzpopmin(["b", "a"], timeout=1) == (b"a", b"a1", 1)
+        assert await r.bzpopmin(["b", "a"], timeout=1) == (b"a", b"a2", 2)
+        assert await r.bzpopmin(["b", "a"], timeout=1) is None
+        await r.zadd("c", {"c1": 100})
+        assert await r.bzpopmin("c", timeout=1) == (b"c", b"c1", 100)
+
+    async def test_zrange(self, r: redis.Redis):
+        await r.zadd("a", {"a1": 1, "a2": 2, "a3": 3})
+        assert await r.zrange("a", 0, 1) == [b"a1", b"a2"]
+        assert await r.zrange("a", 1, 2) == [b"a2", b"a3"]
+
+        # withscores
+        assert await r.zrange("a", 0, 1, withscores=True) == [
+            (b"a1", 1.0),
+            (b"a2", 2.0),
+        ]
+        assert await r.zrange("a", 1, 2, withscores=True) == [
+            (b"a2", 2.0),
+            (b"a3", 3.0),
+        ]
+
+        # custom score function
+        assert await r.zrange("a", 0, 1, withscores=True, score_cast_func=int) == [
+            (b"a1", 1),
+            (b"a2", 2),
+        ]
+
+    @skip_if_server_version_lt("2.8.9")
+    async def test_zrangebylex(self, r: redis.Redis):
+        await r.zadd("a", {"a": 0, "b": 0, "c": 0, "d": 0, "e": 0, "f": 0, "g": 0})
+        assert await r.zrangebylex("a", "-", "[c") == [b"a", b"b", b"c"]
+        assert await r.zrangebylex("a", "-", "(c") == [b"a", b"b"]
+        assert await r.zrangebylex("a", "[aaa", "(g") == [b"b", b"c", b"d", b"e", b"f"]
+        assert await r.zrangebylex("a", "[f", "+") == [b"f", b"g"]
+        assert await r.zrangebylex("a", "-", "+", start=3, num=2) == [b"d", b"e"]
+
+    @skip_if_server_version_lt("2.9.9")
+    async def test_zrevrangebylex(self, r: redis.Redis):
+        await r.zadd("a", {"a": 0, "b": 0, "c": 0, "d": 0, "e": 0, "f": 0, "g": 0})
+        assert await r.zrevrangebylex("a", "[c", "-") == [b"c", b"b", b"a"]
+        assert await r.zrevrangebylex("a", "(c", "-") == [b"b", b"a"]
+        assert await r.zrevrangebylex("a", "(g", "[aaa") == [
+            b"f",
+            b"e",
+            b"d",
+            b"c",
+            b"b",
+        ]
+        assert await r.zrevrangebylex("a", "+", "[f") == [b"g", b"f"]
+        assert await r.zrevrangebylex("a", "+", "-", start=3, num=2) == [b"d", b"c"]
+
+    async def test_zrangebyscore(self, r: redis.Redis):
+        await r.zadd("a", {"a1": 1, "a2": 2, "a3": 3, "a4": 4, "a5": 5})
+        assert await r.zrangebyscore("a", 2, 4) == [b"a2", b"a3", b"a4"]
+
+        # slicing with start/num
+        assert await r.zrangebyscore("a", 2, 4, start=1, num=2) == [b"a3", b"a4"]
+
+        # withscores
+        assert await r.zrangebyscore("a", 2, 4, withscores=True) == [
+            (b"a2", 2.0),
+            (b"a3", 3.0),
+            (b"a4", 4.0),
+        ]
+
+        # custom score function
+        assert await r.zrangebyscore(
+            "a", 2, 4, withscores=True, score_cast_func=int
+        ) == [(b"a2", 2), (b"a3", 3), (b"a4", 4)]
+
+    async def test_zrank(self, r: redis.Redis):
+        await r.zadd("a", {"a1": 1, "a2": 2, "a3": 3, "a4": 4, "a5": 5})
+        assert await r.zrank("a", "a1") == 0
+        assert await r.zrank("a", "a2") == 1
+        assert await r.zrank("a", "a6") is None
+
+    async def test_zrem(self, r: redis.Redis):
+        await r.zadd("a", {"a1": 1, "a2": 2, "a3": 3})
+        assert await r.zrem("a", "a2") == 1
+        assert await r.zrange("a", 0, -1) == [b"a1", b"a3"]
+        assert await r.zrem("a", "b") == 0
+        assert await r.zrange("a", 0, -1) == [b"a1", b"a3"]
+
+    async def test_zrem_multiple_keys(self, r: redis.Redis):
+        await r.zadd("a", {"a1": 1, "a2": 2, "a3": 3})
+        assert await r.zrem("a", "a1", "a2") == 2
+        assert await r.zrange("a", 0, 5) == [b"a3"]
+
+    @skip_if_server_version_lt("2.8.9")
+    async def test_zremrangebylex(self, r: redis.Redis):
+        await r.zadd("a", {"a": 0, "b": 0, "c": 0, "d": 0, "e": 0, "f": 0, "g": 0})
+        assert await r.zremrangebylex("a", "-", "[c") == 3
+        assert await r.zrange("a", 0, -1) == [b"d", b"e", b"f", b"g"]
+        assert await r.zremrangebylex("a", "[f", "+") == 2
+        assert await r.zrange("a", 0, -1) == [b"d", b"e"]
+        assert await r.zremrangebylex("a", "[h", "+") == 0
+        assert await r.zrange("a", 0, -1) == [b"d", b"e"]
+
+    async def test_zremrangebyrank(self, r: redis.Redis):
+        await r.zadd("a", {"a1": 1, "a2": 2, "a3": 3, "a4": 4, "a5": 5})
+        assert await r.zremrangebyrank("a", 1, 3) == 3
+        assert await r.zrange("a", 0, 5) == [b"a1", b"a5"]
+
+    async def test_zremrangebyscore(self, r: redis.Redis):
+        await r.zadd("a", {"a1": 1, "a2": 2, "a3": 3, "a4": 4, "a5": 5})
+        assert await r.zremrangebyscore("a", 2, 4) == 3
+        assert await r.zrange("a", 0, -1) == [b"a1", b"a5"]
+        assert await r.zremrangebyscore("a", 2, 4) == 0
+        assert await r.zrange("a", 0, -1) == [b"a1", b"a5"]
+
+    async def test_zrevrange(self, r: redis.Redis):
+        await r.zadd("a", {"a1": 1, "a2": 2, "a3": 3})
+        assert await r.zrevrange("a", 0, 1) == [b"a3", b"a2"]
+        assert await r.zrevrange("a", 1, 2) == [b"a2", b"a1"]
+
+        # withscores
+        assert await r.zrevrange("a", 0, 1, withscores=True) == [
+            (b"a3", 3.0),
+            (b"a2", 2.0),
+        ]
+        assert await r.zrevrange("a", 1, 2, withscores=True) == [
+            (b"a2", 2.0),
+            (b"a1", 1.0),
+        ]
+
+        # custom score function
+        assert await r.zrevrange("a", 0, 1, withscores=True, score_cast_func=int) == [
+            (b"a3", 3.0),
+            (b"a2", 2.0),
+        ]
+
+    async def test_zrevrangebyscore(self, r: redis.Redis):
+        await r.zadd("a", {"a1": 1, "a2": 2, "a3": 3, "a4": 4, "a5": 5})
+        assert await r.zrevrangebyscore("a", 4, 2) == [b"a4", b"a3", b"a2"]
+
+        # slicing with start/num
+        assert await r.zrevrangebyscore("a", 4, 2, start=1, num=2) == [b"a3", b"a2"]
+
+        # withscores
+        assert await r.zrevrangebyscore("a", 4, 2, withscores=True) == [
+            (b"a4", 4.0),
+            (b"a3", 3.0),
+            (b"a2", 2.0),
+        ]
+
+        # custom score function
+        assert await r.zrevrangebyscore(
+            "a", 4, 2, withscores=True, score_cast_func=int
+        ) == [(b"a4", 4), (b"a3", 3), (b"a2", 2)]
+
+    async def test_zrevrank(self, r: redis.Redis):
+        await r.zadd("a", {"a1": 1, "a2": 2, "a3": 3, "a4": 4, "a5": 5})
+        assert await r.zrevrank("a", "a1") == 4
+        assert await r.zrevrank("a", "a2") == 3
+        assert await r.zrevrank("a", "a6") is None
+
+    async def test_zscore(self, r: redis.Redis):
+        await r.zadd("a", {"a1": 1, "a2": 2, "a3": 3})
+        assert await r.zscore("a", "a1") == 1.0
+        assert await r.zscore("a", "a2") == 2.0
+        assert await r.zscore("a", "a4") is None
+
+    async def test_zunionstore_sum(self, r: redis.Redis):
+        await r.zadd("a", {"a1": 1, "a2": 1, "a3": 1})
+        await r.zadd("b", {"a1": 2, "a2": 2, "a3": 2})
+        await r.zadd("c", {"a1": 6, "a3": 5, "a4": 4})
+        assert await r.zunionstore("d", ["a", "b", "c"]) == 4
+        assert await r.zrange("d", 0, -1, withscores=True) == [
+            (b"a2", 3),
+            (b"a4", 4),
+            (b"a3", 8),
+            (b"a1", 9),
+        ]
+
+    async def test_zunionstore_max(self, r: redis.Redis):
+        await r.zadd("a", {"a1": 1, "a2": 1, "a3": 1})
+        await r.zadd("b", {"a1": 2, "a2": 2, "a3": 2})
+        await r.zadd("c", {"a1": 6, "a3": 5, "a4": 4})
+        assert await r.zunionstore("d", ["a", "b", "c"], aggregate="MAX") == 4
+        assert await r.zrange("d", 0, -1, withscores=True) == [
+            (b"a2", 2),
+            (b"a4", 4),
+            (b"a3", 5),
+            (b"a1", 6),
+        ]
+
+    async def test_zunionstore_min(self, r: redis.Redis):
+        await r.zadd("a", {"a1": 1, "a2": 2, "a3": 3})
+        await r.zadd("b", {"a1": 2, "a2": 2, "a3": 4})
+        await r.zadd("c", {"a1": 6, "a3": 5, "a4": 4})
+        assert await r.zunionstore("d", ["a", "b", "c"], aggregate="MIN") == 4
+        assert await r.zrange("d", 0, -1, withscores=True) == [
+            (b"a1", 1),
+            (b"a2", 2),
+            (b"a3", 3),
+            (b"a4", 4),
+        ]
+
+    async def test_zunionstore_with_weight(self, r: redis.Redis):
+        await r.zadd("a", {"a1": 1, "a2": 1, "a3": 1})
+        await r.zadd("b", {"a1": 2, "a2": 2, "a3": 2})
+        await r.zadd("c", {"a1": 6, "a3": 5, "a4": 4})
+        assert await r.zunionstore("d", {"a": 1, "b": 2, "c": 3}) == 4
+        assert await r.zrange("d", 0, -1, withscores=True) == [
+            (b"a2", 5),
+            (b"a4", 12),
+            (b"a3", 20),
+            (b"a1", 23),
+        ]
+
+    # HYPERLOGLOG TESTS
+    @skip_if_server_version_lt("2.8.9")
+    async def test_pfadd(self, r: redis.Redis):
+        members = {b"1", b"2", b"3"}
+        assert await r.pfadd("a", *members) == 1
+        assert await r.pfadd("a", *members) == 0
+        assert await r.pfcount("a") == len(members)
+
+    @skip_if_server_version_lt("2.8.9")
+    async def test_pfcount(self, r: redis.Redis):
+        members = {b"1", b"2", b"3"}
+        await r.pfadd("a", *members)
+        assert await r.pfcount("a") == len(members)
+        members_b = {b"2", b"3", b"4"}
+        await r.pfadd("b", *members_b)
+        assert await r.pfcount("b") == len(members_b)
+        assert await r.pfcount("a", "b") == len(members_b.union(members))
+
+    @skip_if_server_version_lt("2.8.9")
+    async def test_pfmerge(self, r: redis.Redis):
+        mema = {b"1", b"2", b"3"}
+        memb = {b"2", b"3", b"4"}
+        memc = {b"5", b"6", b"7"}
+        await r.pfadd("a", *mema)
+        await r.pfadd("b", *memb)
+        await r.pfadd("c", *memc)
+        await r.pfmerge("d", "c", "a")
+        assert await r.pfcount("d") == 6
+        await r.pfmerge("d", "b")
+        assert await r.pfcount("d") == 7
+
+    # HASH COMMANDS
+    async def test_hget_and_hset(self, r: redis.Redis):
+        await r.hset("a", mapping={"1": 1, "2": 2, "3": 3})
+        assert await r.hget("a", "1") == b"1"
+        assert await r.hget("a", "2") == b"2"
+        assert await r.hget("a", "3") == b"3"
+
+        # field was updated, redis returns 0
+        assert await r.hset("a", "2", 5) == 0
+        assert await r.hget("a", "2") == b"5"
+
+        # field is new, redis returns 1
+        assert await r.hset("a", "4", 4) == 1
+        assert await r.hget("a", "4") == b"4"
+
+        # key inside of hash that doesn't exist returns null value
+        assert await r.hget("a", "b") is None
+
+        # keys with bool(key) == False
+        assert await r.hset("a", 0, 10) == 1
+        assert await r.hset("a", "", 10) == 1
+
+    async def test_hset_with_multi_key_values(self, r: redis.Redis):
+        await r.hset("a", mapping={"1": 1, "2": 2, "3": 3})
+        assert await r.hget("a", "1") == b"1"
+        assert await r.hget("a", "2") == b"2"
+        assert await r.hget("a", "3") == b"3"
+
+        await r.hset("b", "foo", "bar", mapping={"1": 1, "2": 2})
+        assert await r.hget("b", "1") == b"1"
+        assert await r.hget("b", "2") == b"2"
+        assert await r.hget("b", "foo") == b"bar"
+
+    async def test_hset_without_data(self, r: redis.Redis):
+        with pytest.raises(exceptions.DataError):
+            await r.hset("x")
+
+    async def test_hdel(self, r: redis.Redis):
+        await r.hset("a", mapping={"1": 1, "2": 2, "3": 3})
+        assert await r.hdel("a", "2") == 1
+        assert await r.hget("a", "2") is None
+        assert await r.hdel("a", "1", "3") == 2
+        assert await r.hlen("a") == 0
+
+    async def test_hexists(self, r: redis.Redis):
+        await r.hset("a", mapping={"1": 1, "2": 2, "3": 3})
+        assert await r.hexists("a", "1")
+        assert not await r.hexists("a", "4")
+
+    async def test_hgetall(self, r: redis.Redis):
+        h = {b"a1": b"1", b"a2": b"2", b"a3": b"3"}
+        await r.hset("a", mapping=h)
+        assert await r.hgetall("a") == h
+
+    async def test_hincrby(self, r: redis.Redis):
+        assert await r.hincrby("a", "1") == 1
+        assert await r.hincrby("a", "1", amount=2) == 3
+        assert await r.hincrby("a", "1", amount=-2) == 1
+
+    @skip_if_server_version_lt("2.6.0")
+    async def test_hincrbyfloat(self, r: redis.Redis):
+        assert await r.hincrbyfloat("a", "1") == 1.0
+        assert await r.hincrbyfloat("a", "1") == 2.0
+        assert await r.hincrbyfloat("a", "1", 1.2) == 3.2
+
+    async def test_hkeys(self, r: redis.Redis):
+        h = {b"a1": b"1", b"a2": b"2", b"a3": b"3"}
+        await r.hset("a", mapping=h)
+        local_keys = list(h.keys())
+        remote_keys = await r.hkeys("a")
+        assert sorted(local_keys) == sorted(remote_keys)
+
+    async def test_hlen(self, r: redis.Redis):
+        await r.hset("a", mapping={"1": 1, "2": 2, "3": 3})
+        assert await r.hlen("a") == 3
+
+    async def test_hmget(self, r: redis.Redis):
+        assert await r.hset("a", mapping={"a": 1, "b": 2, "c": 3})
+        assert await r.hmget("a", "a", "b", "c") == [b"1", b"2", b"3"]
+
+    async def test_hmset(self, r: redis.Redis):
+        warning_message = (
+            r"^Redis\.hmset\(\) is deprecated\. " r"Use Redis\.hset\(\) instead\.$"
+        )
+        h = {b"a": b"1", b"b": b"2", b"c": b"3"}
+        with pytest.warns(DeprecationWarning, match=warning_message):
+            assert await r.hmset("a", h)
+        assert await r.hgetall("a") == h
+
+    async def test_hsetnx(self, r: redis.Redis):
+        # Initially set the hash field
+        assert await r.hsetnx("a", "1", 1)
+        assert await r.hget("a", "1") == b"1"
+        assert not await r.hsetnx("a", "1", 2)
+        assert await r.hget("a", "1") == b"1"
+
+    async def test_hvals(self, r: redis.Redis):
+        h = {b"a1": b"1", b"a2": b"2", b"a3": b"3"}
+        await r.hset("a", mapping=h)
+        local_vals = list(h.values())
+        remote_vals = await r.hvals("a")
+        assert sorted(local_vals) == sorted(remote_vals)
+
+    @skip_if_server_version_lt("3.2.0")
+    async def test_hstrlen(self, r: redis.Redis):
+        await r.hset("a", mapping={"1": "22", "2": "333"})
+        assert await r.hstrlen("a", "1") == 2
+        assert await r.hstrlen("a", "2") == 3
+
+    # SORT
+    async def test_sort_basic(self, r: redis.Redis):
+        await r.rpush("a", "3", "2", "1", "4")
+        assert await r.sort("a") == [b"1", b"2", b"3", b"4"]
+
+    async def test_sort_limited(self, r: redis.Redis):
+        await r.rpush("a", "3", "2", "1", "4")
+        assert await r.sort("a", start=1, num=2) == [b"2", b"3"]
+
+    async def test_sort_by(self, r: redis.Redis):
+        await r.set("score:1", 8)
+        await r.set("score:2", 3)
+        await r.set("score:3", 5)
+        await r.rpush("a", "3", "2", "1")
+        assert await r.sort("a", by="score:*") == [b"2", b"3", b"1"]
+
+    async def test_sort_get(self, r: redis.Redis):
+        await r.set("user:1", "u1")
+        await r.set("user:2", "u2")
+        await r.set("user:3", "u3")
+        await r.rpush("a", "2", "3", "1")
+        assert await r.sort("a", get="user:*") == [b"u1", b"u2", b"u3"]
+
+    async def test_sort_get_multi(self, r: redis.Redis):
+        await r.set("user:1", "u1")
+        await r.set("user:2", "u2")
+        await r.set("user:3", "u3")
+        await r.rpush("a", "2", "3", "1")
+        assert await r.sort("a", get=("user:*", "#")) == [
+            b"u1",
+            b"1",
+            b"u2",
+            b"2",
+            b"u3",
+            b"3",
+        ]
+
+    async def test_sort_get_groups_two(self, r: redis.Redis):
+        await r.set("user:1", "u1")
+        await r.set("user:2", "u2")
+        await r.set("user:3", "u3")
+        await r.rpush("a", "2", "3", "1")
+        assert await r.sort("a", get=("user:*", "#"), groups=True) == [
+            (b"u1", b"1"),
+            (b"u2", b"2"),
+            (b"u3", b"3"),
+        ]
+
+    async def test_sort_groups_string_get(self, r: redis.Redis):
+        await r.set("user:1", "u1")
+        await r.set("user:2", "u2")
+        await r.set("user:3", "u3")
+        await r.rpush("a", "2", "3", "1")
+        with pytest.raises(exceptions.DataError):
+            await r.sort("a", get="user:*", groups=True)
+
+    async def test_sort_groups_just_one_get(self, r: redis.Redis):
+        await r.set("user:1", "u1")
+        await r.set("user:2", "u2")
+        await r.set("user:3", "u3")
+        await r.rpush("a", "2", "3", "1")
+        with pytest.raises(exceptions.DataError):
+            await r.sort("a", get=["user:*"], groups=True)
+
+    async def test_sort_groups_no_get(self, r: redis.Redis):
+        await r.set("user:1", "u1")
+        await r.set("user:2", "u2")
+        await r.set("user:3", "u3")
+        await r.rpush("a", "2", "3", "1")
+        with pytest.raises(exceptions.DataError):
+            await r.sort("a", groups=True)
+
+    async def test_sort_groups_three_gets(self, r: redis.Redis):
+        await r.set("user:1", "u1")
+        await r.set("user:2", "u2")
+        await r.set("user:3", "u3")
+        await r.set("door:1", "d1")
+        await r.set("door:2", "d2")
+        await r.set("door:3", "d3")
+        await r.rpush("a", "2", "3", "1")
+        assert await r.sort("a", get=("user:*", "door:*", "#"), groups=True) == [
+            (b"u1", b"d1", b"1"),
+            (b"u2", b"d2", b"2"),
+            (b"u3", b"d3", b"3"),
+        ]
+
+    async def test_sort_desc(self, r: redis.Redis):
+        await r.rpush("a", "2", "3", "1")
+        assert await r.sort("a", desc=True) == [b"3", b"2", b"1"]
+
+    async def test_sort_alpha(self, r: redis.Redis):
+        await r.rpush("a", "e", "c", "b", "d", "a")
+        assert await r.sort("a", alpha=True) == [b"a", b"b", b"c", b"d", b"e"]
+
+    async def test_sort_store(self, r: redis.Redis):
+        await r.rpush("a", "2", "3", "1")
+        assert await r.sort("a", store="sorted_values") == 3
+        assert await r.lrange("sorted_values", 0, -1) == [b"1", b"2", b"3"]
+
+    async def test_sort_all_options(self, r: redis.Redis):
+        await r.set("user:1:username", "zeus")
+        await r.set("user:2:username", "titan")
+        await r.set("user:3:username", "hermes")
+        await r.set("user:4:username", "hercules")
+        await r.set("user:5:username", "apollo")
+        await r.set("user:6:username", "athena")
+        await r.set("user:7:username", "hades")
+        await r.set("user:8:username", "dionysus")
+
+        await r.set("user:1:favorite_drink", "yuengling")
+        await r.set("user:2:favorite_drink", "rum")
+        await r.set("user:3:favorite_drink", "vodka")
+        await r.set("user:4:favorite_drink", "milk")
+        await r.set("user:5:favorite_drink", "pinot noir")
+        await r.set("user:6:favorite_drink", "water")
+        await r.set("user:7:favorite_drink", "gin")
+        await r.set("user:8:favorite_drink", "apple juice")
+
+        await r.rpush("gods", "5", "8", "3", "1", "2", "7", "6", "4")
+        num = await r.sort(
+            "gods",
+            start=2,
+            num=4,
+            by="user:*:username",
+            get="user:*:favorite_drink",
+            desc=True,
+            alpha=True,
+            store="sorted",
+        )
+        assert num == 4
+        assert await r.lrange("sorted", 0, 10) == [
+            b"vodka",
+            b"milk",
+            b"gin",
+            b"apple juice",
+        ]
+
+    async def test_sort_issue_924(self, r: redis.Redis):
+        # Tests for issue https://github.com/andymccurdy/redis-py/issues/924
+        await r.execute_command("SADD", "issue#924", 1)
+        await r.execute_command("SORT", "issue#924")
+
+    async def test_cluster_addslots(self, mock_cluster_resp_ok):
+        assert await mock_cluster_resp_ok.cluster("ADDSLOTS", 1) is True
+
+    async def test_cluster_count_failure_reports(self, mock_cluster_resp_int):
+        assert isinstance(
+            await mock_cluster_resp_int.cluster("COUNT-FAILURE-REPORTS", "node"), int
+        )
+
+    async def test_cluster_countkeysinslot(self, mock_cluster_resp_int):
+        assert isinstance(
+            await mock_cluster_resp_int.cluster("COUNTKEYSINSLOT", 2), int
+        )
+
+    async def test_cluster_delslots(self, mock_cluster_resp_ok):
+        assert await mock_cluster_resp_ok.cluster("DELSLOTS", 1) is True
+
+    async def test_cluster_failover(self, mock_cluster_resp_ok):
+        assert await mock_cluster_resp_ok.cluster("FAILOVER", 1) is True
+
+    async def test_cluster_forget(self, mock_cluster_resp_ok):
+        assert await mock_cluster_resp_ok.cluster("FORGET", 1) is True
+
+    async def test_cluster_info(self, mock_cluster_resp_info):
+        assert isinstance(await mock_cluster_resp_info.cluster("info"), dict)
+
+    async def test_cluster_keyslot(self, mock_cluster_resp_int):
+        assert isinstance(await mock_cluster_resp_int.cluster("keyslot", "asdf"), int)
+
+    async def test_cluster_meet(self, mock_cluster_resp_ok):
+        assert await mock_cluster_resp_ok.cluster("meet", "ip", "port", 1) is True
+
+    async def test_cluster_nodes(self, mock_cluster_resp_nodes):
+        assert isinstance(await mock_cluster_resp_nodes.cluster("nodes"), dict)
+
+    async def test_cluster_replicate(self, mock_cluster_resp_ok):
+        assert await mock_cluster_resp_ok.cluster("replicate", "nodeid") is True
+
+    async def test_cluster_reset(self, mock_cluster_resp_ok):
+        assert await mock_cluster_resp_ok.cluster("reset", "hard") is True
+
+    async def test_cluster_saveconfig(self, mock_cluster_resp_ok):
+        assert await mock_cluster_resp_ok.cluster("saveconfig") is True
+
+    async def test_cluster_setslot(self, mock_cluster_resp_ok):
+        assert (
+            await mock_cluster_resp_ok.cluster("setslot", 1, "IMPORTING", "nodeid")
+            is True
+        )
+
+    async def test_cluster_slaves(self, mock_cluster_resp_slaves):
+        assert isinstance(
+            await mock_cluster_resp_slaves.cluster("slaves", "nodeid"), dict
+        )
+
+    @skip_if_server_version_lt("3.0.0")
+    async def test_readwrite(self, r: redis.Redis):
+        assert await r.readwrite()
+
+    @skip_if_server_version_lt("3.0.0")
+    async def test_readonly_invalid_cluster_state(self, r: redis.Redis):
+        with pytest.raises(exceptions.RedisError):
+            await r.readonly()
+
+    @skip_if_server_version_lt("3.0.0")
+    async def test_readonly(self, mock_cluster_resp_ok):
+        assert await mock_cluster_resp_ok.readonly() is True
+
+    # GEO COMMANDS
+    @skip_if_server_version_lt("3.2.0")
+    async def test_geoadd(self, r: redis.Redis):
+        values = (2.1909389952632, 41.433791470673, "place1") + (
+            2.1873744593677,
+            41.406342043777,
+            "place2",
+        )
+
+        assert await r.geoadd("barcelona", values) == 2
+        assert await r.zcard("barcelona") == 2
+
+    @skip_if_server_version_lt("3.2.0")
+    async def test_geoadd_invalid_params(self, r: redis.Redis):
+        with pytest.raises(exceptions.RedisError):
+            await r.geoadd("barcelona", (1, 2))
+
+    @skip_if_server_version_lt("3.2.0")
+    async def test_geodist(self, r: redis.Redis):
+        values = (2.1909389952632, 41.433791470673, "place1") + (
+            2.1873744593677,
+            41.406342043777,
+            "place2",
+        )
+
+        assert await r.geoadd("barcelona", values) == 2
+        assert await r.geodist("barcelona", "place1", "place2") == 3067.4157
+
+    @skip_if_server_version_lt("3.2.0")
+    async def test_geodist_units(self, r: redis.Redis):
+        values = (2.1909389952632, 41.433791470673, "place1") + (
+            2.1873744593677,
+            41.406342043777,
+            "place2",
+        )
+
+        await r.geoadd("barcelona", values)
+        assert await r.geodist("barcelona", "place1", "place2", "km") == 3.0674
+
+    @skip_if_server_version_lt("3.2.0")
+    async def test_geodist_missing_one_member(self, r: redis.Redis):
+        values = (2.1909389952632, 41.433791470673, "place1")
+        await r.geoadd("barcelona", values)
+        assert await r.geodist("barcelona", "place1", "missing_member", "km") is None
+
+    @skip_if_server_version_lt("3.2.0")
+    async def test_geodist_invalid_units(self, r: redis.Redis):
+        with pytest.raises(exceptions.RedisError):
+            assert await r.geodist("x", "y", "z", "inches")
+
+    @skip_if_server_version_lt("3.2.0")
+    async def test_geohash(self, r: redis.Redis):
+        values = (2.1909389952632, 41.433791470673, "place1") + (
+            2.1873744593677,
+            41.406342043777,
+            "place2",
+        )
+
+        await r.geoadd("barcelona", values)
+        assert await r.geohash("barcelona", "place1", "place2", "place3") == [
+            "sp3e9yg3kd0",
+            "sp3e9cbc3t0",
+            None,
+        ]
+
+    @skip_if_server_version_lt("3.2.0")
+    async def test_geopos(self, r: redis.Redis):
+        values = (2.1909389952632, 41.433791470673, "place1") + (
+            2.1873744593677,
+            41.406342043777,
+            "place2",
+        )
+
+        await r.geoadd("barcelona", values)
+        # redis uses 52 bits precision, hereby small errors may be introduced.
+        assert await r.geopos("barcelona", "place1", "place2") == [
+            (2.19093829393386841, 41.43379028184083523),
+            (2.18737632036209106, 41.40634178640635099),
+        ]
+
+    @skip_if_server_version_lt("4.0.0")
+    async def test_geopos_no_value(self, r: redis.Redis):
+        assert await r.geopos("barcelona", "place1", "place2") == [None, None]
+
+    @skip_if_server_version_lt("3.2.0")
+    @skip_if_server_version_gte("4.0.0")
+    async def test_old_geopos_no_value(self, r: redis.Redis):
+        assert await r.geopos("barcelona", "place1", "place2") == []
+
+    @skip_if_server_version_lt("3.2.0")
+    async def test_georadius(self, r: redis.Redis):
+        values = (2.1909389952632, 41.433791470673, "place1") + (
+            2.1873744593677,
+            41.406342043777,
+            b"\x80place2",
+        )
+
+        await r.geoadd("barcelona", values)
+        assert await r.georadius("barcelona", 2.191, 41.433, 1000) == [b"place1"]
+        assert await r.georadius("barcelona", 2.187, 41.406, 1000) == [b"\x80place2"]
+
+    @skip_if_server_version_lt("3.2.0")
+    async def test_georadius_no_values(self, r: redis.Redis):
+        values = (2.1909389952632, 41.433791470673, "place1") + (
+            2.1873744593677,
+            41.406342043777,
+            "place2",
+        )
+
+        await r.geoadd("barcelona", values)
+        assert await r.georadius("barcelona", 1, 2, 1000) == []
+
+    @skip_if_server_version_lt("3.2.0")
+    async def test_georadius_units(self, r: redis.Redis):
+        values = (2.1909389952632, 41.433791470673, "place1") + (
+            2.1873744593677,
+            41.406342043777,
+            "place2",
+        )
+
+        await r.geoadd("barcelona", values)
+        assert await r.georadius("barcelona", 2.191, 41.433, 1, unit="km") == [
+            b"place1"
+        ]
+
+    @skip_unless_arch_bits(64)
+    @skip_if_server_version_lt("3.2.0")
+    async def test_georadius_with(self, r: redis.Redis):
+        values = (2.1909389952632, 41.433791470673, "place1") + (
+            2.1873744593677,
+            41.406342043777,
+            "place2",
+        )
+
+        await r.geoadd("barcelona", values)
+
+        # test a bunch of combinations to test the parse response
+        # function.
+        assert await r.georadius(
+            "barcelona",
+            2.191,
+            41.433,
+            1,
+            unit="km",
+            withdist=True,
+            withcoord=True,
+            withhash=True,
+        ) == [
+            [
+                b"place1",
+                0.0881,
+                3471609698139488,
+                (2.19093829393386841, 41.43379028184083523),
+            ]
+        ]
+
+        assert await r.georadius(
+            "barcelona", 2.191, 41.433, 1, unit="km", withdist=True, withcoord=True
+        ) == [[b"place1", 0.0881, (2.19093829393386841, 41.43379028184083523)]]
+
+        assert await r.georadius(
+            "barcelona", 2.191, 41.433, 1, unit="km", withhash=True, withcoord=True
+        ) == [
+            [b"place1", 3471609698139488, (2.19093829393386841, 41.43379028184083523)]
+        ]
+
+        # test no values.
+        assert (
+            await r.georadius(
+                "barcelona",
+                2,
+                1,
+                1,
+                unit="km",
+                withdist=True,
+                withcoord=True,
+                withhash=True,
+            )
+            == []
+        )
+
+    @skip_if_server_version_lt("3.2.0")
+    async def test_georadius_count(self, r: redis.Redis):
+        values = (2.1909389952632, 41.433791470673, "place1") + (
+            2.1873744593677,
+            41.406342043777,
+            "place2",
+        )
+
+        await r.geoadd("barcelona", values)
+        assert await r.georadius("barcelona", 2.191, 41.433, 3000, count=1) == [
+            b"place1"
+        ]
+
+    @skip_if_server_version_lt("3.2.0")
+    async def test_georadius_sort(self, r: redis.Redis):
+        values = (2.1909389952632, 41.433791470673, "place1") + (
+            2.1873744593677,
+            41.406342043777,
+            "place2",
+        )
+
+        await r.geoadd("barcelona", values)
+        assert await r.georadius("barcelona", 2.191, 41.433, 3000, sort="ASC") == [
+            b"place1",
+            b"place2",
+        ]
+        assert await r.georadius("barcelona", 2.191, 41.433, 3000, sort="DESC") == [
+            b"place2",
+            b"place1",
+        ]
+
+    @skip_if_server_version_lt("3.2.0")
+    async def test_georadius_store(self, r: redis.Redis):
+        values = (2.1909389952632, 41.433791470673, "place1") + (
+            2.1873744593677,
+            41.406342043777,
+            "place2",
+        )
+
+        await r.geoadd("barcelona", values)
+        await r.georadius("barcelona", 2.191, 41.433, 1000, store="places_barcelona")
+        assert await r.zrange("places_barcelona", 0, -1) == [b"place1"]
+
+    @skip_unless_arch_bits(64)
+    @skip_if_server_version_lt("3.2.0")
+    async def test_georadius_store_dist(self, r: redis.Redis):
+        values = (2.1909389952632, 41.433791470673, "place1") + (
+            2.1873744593677,
+            41.406342043777,
+            "place2",
+        )
+
+        await r.geoadd("barcelona", values)
+        await r.georadius(
+            "barcelona", 2.191, 41.433, 1000, store_dist="places_barcelona"
+        )
+        # instead of save the geo score, the distance is saved.
+        assert await r.zscore("places_barcelona", "place1") == 88.05060698409301
+
+    @skip_unless_arch_bits(64)
+    @skip_if_server_version_lt("3.2.0")
+    async def test_georadiusmember(self, r: redis.Redis):
+        values = (2.1909389952632, 41.433791470673, "place1") + (
+            2.1873744593677,
+            41.406342043777,
+            b"\x80place2",
+        )
+
+        await r.geoadd("barcelona", values)
+        assert await r.georadiusbymember("barcelona", "place1", 4000) == [
+            b"\x80place2",
+            b"place1",
+        ]
+        assert await r.georadiusbymember("barcelona", "place1", 10) == [b"place1"]
+
+        assert await r.georadiusbymember(
+            "barcelona", "place1", 4000, withdist=True, withcoord=True, withhash=True
+        ) == [
+            [
+                b"\x80place2",
+                3067.4157,
+                3471609625421029,
+                (2.187376320362091, 41.40634178640635),
+            ],
+            [
+                b"place1",
+                0.0,
+                3471609698139488,
+                (2.1909382939338684, 41.433790281840835),
+            ],
+        ]
+
+    @skip_if_server_version_lt("5.0.0")
+    async def test_xack(self, r: redis.Redis):
+        stream = "stream"
+        group = "group"
+        consumer = "consumer"
+        # xack on a stream that doesn't exist
+        assert await r.xack(stream, group, "0-0") == 0
+
+        m1 = await r.xadd(stream, {"one": "one"})
+        m2 = await r.xadd(stream, {"two": "two"})
+        m3 = await r.xadd(stream, {"three": "three"})
+
+        # xack on a group that doesn't exist
+        assert await r.xack(stream, group, m1) == 0
+
+        await r.xgroup_create(stream, group, 0)
+        await r.xreadgroup(group, consumer, streams={stream: ">"})
+        # xack returns the number of ack'd elements
+        assert await r.xack(stream, group, m1) == 1
+        assert await r.xack(stream, group, m2, m3) == 2
+
+    @skip_if_server_version_lt("5.0.0")
+    async def test_xadd(self, r: redis.Redis):
+        stream = "stream"
+        message_id = await r.xadd(stream, {"foo": "bar"})
+        assert re.match(rb"[0-9]+\-[0-9]+", message_id)
+
+        # explicit message id
+        message_id = b"9999999999999999999-0"
+        assert message_id == await r.xadd(stream, {"foo": "bar"}, id=message_id)
+
+        # with maxlen, the list evicts the first message
+        await r.xadd(stream, {"foo": "bar"}, maxlen=2, approximate=False)
+        assert await r.xlen(stream) == 2
+
+    @skip_if_server_version_lt("5.0.0")
+    async def test_xclaim(self, r: redis.Redis):
+        stream = "stream"
+        group = "group"
+        consumer1 = "consumer1"
+        consumer2 = "consumer2"
+
+        message_id = await r.xadd(stream, {"john": "wick"})
+        message = await get_stream_message(r, stream, message_id)
+        await r.xgroup_create(stream, group, 0)
+
+        # trying to claim a message that isn't already pending doesn't
+        # do anything
+        response = await r.xclaim(
+            stream, group, consumer2, min_idle_time=0, message_ids=(message_id,)
+        )
+        assert response == []
+
+        # read the group as consumer1 to initially claim the messages
+        await r.xreadgroup(group, consumer1, streams={stream: ">"})
+
+        # claim the message as consumer2
+        response = await r.xclaim(
+            stream, group, consumer2, min_idle_time=0, message_ids=(message_id,)
+        )
+        assert response[0] == message
+
+        # reclaim the message as consumer1, but use the justid argument
+        # which only returns message ids
+        assert (
+            await r.xclaim(
+                stream,
+                group,
+                consumer1,
+                min_idle_time=0,
+                message_ids=(message_id,),
+                justid=True,
+            )
+            == [message_id]
+        )
+
+    @skip_if_server_version_lt("5.0.0")
+    async def test_xclaim_trimmed(self, r: redis.Redis):
+        # xclaim should not raise an exception if the item is not there
+        stream = "stream"
+        group = "group"
+
+        await r.xgroup_create(stream, group, id="$", mkstream=True)
+
+        # add a couple of new items
+        sid1 = await r.xadd(stream, {"item": 0})
+        sid2 = await r.xadd(stream, {"item": 0})
+
+        # read them from consumer1
+        await r.xreadgroup(group, "consumer1", {stream: ">"})
+
+        # add a 3rd and trim the stream down to 2 items
+        await r.xadd(stream, {"item": 3}, maxlen=2, approximate=False)
+
+        # xclaim them from consumer2
+        # the item that is still in the stream should be returned
+        item = await r.xclaim(stream, group, "consumer2", 0, [sid1, sid2])
+        assert len(item) == 2
+        assert item[0] == (None, None)
+        assert item[1][0] == sid2
+
+    @skip_if_server_version_lt("5.0.0")
+    async def test_xdel(self, r: redis.Redis):
+        stream = "stream"
+
+        # deleting from an empty stream doesn't do anything
+        assert await r.xdel(stream, 1) == 0
+
+        m1 = await r.xadd(stream, {"foo": "bar"})
+        m2 = await r.xadd(stream, {"foo": "bar"})
+        m3 = await r.xadd(stream, {"foo": "bar"})
+
+        # xdel returns the number of deleted elements
+        assert await r.xdel(stream, m1) == 1
+        assert await r.xdel(stream, m2, m3) == 2
+
+    @skip_if_server_version_lt("5.0.0")
+    async def test_xgroup_create(self, r: redis.Redis):
+        # tests xgroup_create and xinfo_groups
+        stream = "stream"
+        group = "group"
+        await r.xadd(stream, {"foo": "bar"})
+
+        # no group is setup yet, no info to obtain
+        assert await r.xinfo_groups(stream) == []
+
+        assert await r.xgroup_create(stream, group, 0)
+        expected = [
+            {
+                "name": group.encode(),
+                "consumers": 0,
+                "pending": 0,
+                "last-delivered-id": b"0-0",
+            }
+        ]
+        assert await r.xinfo_groups(stream) == expected
+
+    @skip_if_server_version_lt("5.0.0")
+    async def test_xgroup_create_mkstream(self, r: redis.Redis):
+        # tests xgroup_create and xinfo_groups
+        stream = "stream"
+        group = "group"
+
+        # an error is raised if a group is created on a stream that
+        # doesn't already exist
+        with pytest.raises(exceptions.ResponseError):
+            await r.xgroup_create(stream, group, 0)
+
+        # however, with mkstream=True, the underlying stream is created
+        # automatically
+        assert await r.xgroup_create(stream, group, 0, mkstream=True)
+        expected = [
+            {
+                "name": group.encode(),
+                "consumers": 0,
+                "pending": 0,
+                "last-delivered-id": b"0-0",
+            }
+        ]
+        assert await r.xinfo_groups(stream) == expected
+
+    @skip_if_server_version_lt("5.0.0")
+    async def test_xgroup_delconsumer(self, r: redis.Redis):
+        stream = "stream"
+        group = "group"
+        consumer = "consumer"
+        await r.xadd(stream, {"foo": "bar"})
+        await r.xadd(stream, {"foo": "bar"})
+        await r.xgroup_create(stream, group, 0)
+
+        # a consumer that hasn't yet read any messages doesn't do anything
+        assert await r.xgroup_delconsumer(stream, group, consumer) == 0
+
+        # read all messages from the group
+        await r.xreadgroup(group, consumer, streams={stream: ">"})
+
+        # deleting the consumer should return 2 pending messages
+        assert await r.xgroup_delconsumer(stream, group, consumer) == 2
+
+    @skip_if_server_version_lt("5.0.0")
+    async def test_xgroup_destroy(self, r: redis.Redis):
+        stream = "stream"
+        group = "group"
+        await r.xadd(stream, {"foo": "bar"})
+
+        # destroying a nonexistent group returns False
+        assert not await r.xgroup_destroy(stream, group)
+
+        await r.xgroup_create(stream, group, 0)
+        assert await r.xgroup_destroy(stream, group)
+
+    @skip_if_server_version_lt("5.0.0")
+    async def test_xgroup_setid(self, r: redis.Redis):
+        stream = "stream"
+        group = "group"
+        message_id = await r.xadd(stream, {"foo": "bar"})
+
+        await r.xgroup_create(stream, group, 0)
+        # advance the last_delivered_id to the message_id
+        await r.xgroup_setid(stream, group, message_id)
+        expected = [
+            {
+                "name": group.encode(),
+                "consumers": 0,
+                "pending": 0,
+                "last-delivered-id": message_id,
+            }
+        ]
+        assert await r.xinfo_groups(stream) == expected
+
+    @skip_if_server_version_lt("5.0.0")
+    async def test_xinfo_consumers(self, r: redis.Redis):
+        stream = "stream"
+        group = "group"
+        consumer1 = "consumer1"
+        consumer2 = "consumer2"
+        await r.xadd(stream, {"foo": "bar"})
+        await r.xadd(stream, {"foo": "bar"})
+        await r.xadd(stream, {"foo": "bar"})
+
+        await r.xgroup_create(stream, group, 0)
+        await r.xreadgroup(group, consumer1, streams={stream: ">"}, count=1)
+        await r.xreadgroup(group, consumer2, streams={stream: ">"})
+        info = await r.xinfo_consumers(stream, group)
+        assert len(info) == 2
+        expected = [
+            {"name": consumer1.encode(), "pending": 1},
+            {"name": consumer2.encode(), "pending": 2},
+        ]
+
+        # we can't determine the idle time, so just make sure it's an int
+        assert isinstance(info[0].pop("idle"), int)
+        assert isinstance(info[1].pop("idle"), int)
+        assert info == expected
+
+    @skip_if_server_version_lt("5.0.0")
+    async def test_xinfo_stream(self, r: redis.Redis):
+        stream = "stream"
+        m1 = await r.xadd(stream, {"foo": "bar"})
+        m2 = await r.xadd(stream, {"foo": "bar"})
+        info = await r.xinfo_stream(stream)
+
+        assert info["length"] == 2
+        assert info["first-entry"] == await get_stream_message(r, stream, m1)
+        assert info["last-entry"] == await get_stream_message(r, stream, m2)
+
+    @skip_if_server_version_lt("5.0.0")
+    async def test_xlen(self, r: redis.Redis):
+        stream = "stream"
+        assert await r.xlen(stream) == 0
+        await r.xadd(stream, {"foo": "bar"})
+        await r.xadd(stream, {"foo": "bar"})
+        assert await r.xlen(stream) == 2
+
+    @skip_if_server_version_lt("5.0.0")
+    async def test_xpending(self, r: redis.Redis):
+        stream = "stream"
+        group = "group"
+        consumer1 = "consumer1"
+        consumer2 = "consumer2"
+        m1 = await r.xadd(stream, {"foo": "bar"})
+        m2 = await r.xadd(stream, {"foo": "bar"})
+        await r.xgroup_create(stream, group, 0)
+
+        # xpending on a group that has no consumers yet
+        expected = {"pending": 0, "min": None, "max": None, "consumers": []}
+        assert await r.xpending(stream, group) == expected
+
+        # read 1 message from the group with each consumer
+        await r.xreadgroup(group, consumer1, streams={stream: ">"}, count=1)
+        await r.xreadgroup(group, consumer2, streams={stream: ">"}, count=1)
+
+        expected = {
+            "pending": 2,
+            "min": m1,
+            "max": m2,
+            "consumers": [
+                {"name": consumer1.encode(), "pending": 1},
+                {"name": consumer2.encode(), "pending": 1},
+            ],
+        }
+        assert await r.xpending(stream, group) == expected
+
+    @skip_if_server_version_lt("5.0.0")
+    async def test_xpending_range(self, r: redis.Redis):
+        stream = "stream"
+        group = "group"
+        consumer1 = "consumer1"
+        consumer2 = "consumer2"
+        m1 = await r.xadd(stream, {"foo": "bar"})
+        m2 = await r.xadd(stream, {"foo": "bar"})
+        await r.xgroup_create(stream, group, 0)
+
+        # xpending range on a group that has no consumers yet
+        assert await r.xpending_range(stream, group, min="-", max="+", count=5) == []
+
+        # read 1 message from the group with each consumer
+        await r.xreadgroup(group, consumer1, streams={stream: ">"}, count=1)
+        await r.xreadgroup(group, consumer2, streams={stream: ">"}, count=1)
+
+        response = await r.xpending_range(stream, group, min="-", max="+", count=5)
+        assert len(response) == 2
+        assert response[0]["message_id"] == m1
+        assert response[0]["consumer"] == consumer1.encode()
+        assert response[1]["message_id"] == m2
+        assert response[1]["consumer"] == consumer2.encode()
+
+    @skip_if_server_version_lt("5.0.0")
+    async def test_xrange(self, r: redis.Redis):
+        stream = "stream"
+        m1 = await r.xadd(stream, {"foo": "bar"})
+        m2 = await r.xadd(stream, {"foo": "bar"})
+        m3 = await r.xadd(stream, {"foo": "bar"})
+        m4 = await r.xadd(stream, {"foo": "bar"})
+
+        def get_ids(results):
+            return [result[0] for result in results]
+
+        results = await r.xrange(stream, min=m1)
+        assert get_ids(results) == [m1, m2, m3, m4]
+
+        results = await r.xrange(stream, min=m2, max=m3)
+        assert get_ids(results) == [m2, m3]
+
+        results = await r.xrange(stream, max=m3)
+        assert get_ids(results) == [m1, m2, m3]
+
+        results = await r.xrange(stream, max=m2, count=1)
+        assert get_ids(results) == [m1]
+
+    @skip_if_server_version_lt("5.0.0")
+    async def test_xread(self, r: redis.Redis):
+        stream = "stream"
+        m1 = await r.xadd(stream, {"foo": "bar"})
+        m2 = await r.xadd(stream, {"bing": "baz"})
+
+        expected = [
+            [
+                stream.encode(),
+                [
+                    await get_stream_message(r, stream, m1),
+                    await get_stream_message(r, stream, m2),
+                ],
+            ]
+        ]
+        # xread starting at 0 returns both messages
+        assert await r.xread(streams={stream: 0}) == expected
+
+        expected = [
+            [
+                stream.encode(),
+                [
+                    await get_stream_message(r, stream, m1),
+                ],
+            ]
+        ]
+        # xread starting at 0 and count=1 returns only the first message
+        assert await r.xread(streams={stream: 0}, count=1) == expected
+
+        expected = [
+            [
+                stream.encode(),
+                [
+                    await get_stream_message(r, stream, m2),
+                ],
+            ]
+        ]
+        # xread starting at m1 returns only the second message
+        assert await r.xread(streams={stream: m1}) == expected
+
+        # xread starting at the last message returns an empty list
+        assert await r.xread(streams={stream: m2}) == []
+
+    @skip_if_server_version_lt("5.0.0")
+    async def test_xreadgroup(self, r: redis.Redis):
+        stream = "stream"
+        group = "group"
+        consumer = "consumer"
+        m1 = await r.xadd(stream, {"foo": "bar"})
+        m2 = await r.xadd(stream, {"bing": "baz"})
+        await r.xgroup_create(stream, group, 0)
+
+        expected = [
+            [
+                stream.encode(),
+                [
+                    await get_stream_message(r, stream, m1),
+                    await get_stream_message(r, stream, m2),
+                ],
+            ]
+        ]
+        # xread starting at 0 returns both messages
+        assert await r.xreadgroup(group, consumer, streams={stream: ">"}) == expected
+
+        await r.xgroup_destroy(stream, group)
+        await r.xgroup_create(stream, group, 0)
+
+        expected = [
+            [
+                stream.encode(),
+                [
+                    await get_stream_message(r, stream, m1),
+                ],
+            ]
+        ]
+        # xread with count=1 returns only the first message
+        assert (
+            await r.xreadgroup(group, consumer, streams={stream: ">"}, count=1)
+            == expected
+        )
+
+        await r.xgroup_destroy(stream, group)
+
+        # create the group using $ as the last id meaning subsequent reads
+        # will only find messages added after this
+        await r.xgroup_create(stream, group, "$")
+
+        expected = []
+        # xread starting after the last message returns an empty message list
+        assert await r.xreadgroup(group, consumer, streams={stream: ">"}) == expected
+
+        # xreadgroup with noack does not have any items in the PEL
+        await r.xgroup_destroy(stream, group)
+        await r.xgroup_create(stream, group, "0")
+        assert (
+            len(
+                (
+                    await r.xreadgroup(
+                        group, consumer, streams={stream: ">"}, noack=True
+                    )
+                )[0][1]
+            )
+            == 2
+        )
+        # now there should be nothing pending
+        assert (
+            len((await r.xreadgroup(group, consumer, streams={stream: "0"}))[0][1]) == 0
+        )
+
+        await r.xgroup_destroy(stream, group)
+        await r.xgroup_create(stream, group, "0")
+        # delete all the messages in the stream
+        expected = [
+            [
+                stream.encode(),
+                [
+                    (m1, {}),
+                    (m2, {}),
+                ],
+            ]
+        ]
+        await r.xreadgroup(group, consumer, streams={stream: ">"})
+        await r.xtrim(stream, 0)
+        assert await r.xreadgroup(group, consumer, streams={stream: "0"}) == expected
+
+    @skip_if_server_version_lt("5.0.0")
+    async def test_xrevrange(self, r: redis.Redis):
+        stream = "stream"
+        m1 = await r.xadd(stream, {"foo": "bar"})
+        m2 = await r.xadd(stream, {"foo": "bar"})
+        m3 = await r.xadd(stream, {"foo": "bar"})
+        m4 = await r.xadd(stream, {"foo": "bar"})
+
+        def get_ids(results):
+            return [result[0] for result in results]
+
+        results = await r.xrevrange(stream, max=m4)
+        assert get_ids(results) == [m4, m3, m2, m1]
+
+        results = await r.xrevrange(stream, max=m3, min=m2)
+        assert get_ids(results) == [m3, m2]
+
+        results = await r.xrevrange(stream, min=m3)
+        assert get_ids(results) == [m4, m3]
+
+        results = await r.xrevrange(stream, min=m2, count=1)
+        assert get_ids(results) == [m4]
+
+    @skip_if_server_version_lt("5.0.0")
+    async def test_xtrim(self, r: redis.Redis):
+        stream = "stream"
+
+        # trimming an empty key doesn't do anything
+        assert await r.xtrim(stream, 1000) == 0
+
+        await r.xadd(stream, {"foo": "bar"})
+        await r.xadd(stream, {"foo": "bar"})
+        await r.xadd(stream, {"foo": "bar"})
+        await r.xadd(stream, {"foo": "bar"})
+
+        # trimming an amount large than the number of messages
+        # doesn't do anything
+        assert await r.xtrim(stream, 5, approximate=False) == 0
+
+        # 1 message is trimmed
+        assert await r.xtrim(stream, 3, approximate=False) == 1
+
+    async def test_bitfield_operations(self, r: redis.Redis):
+        # comments show affected bits
+        await r.execute_command("SELECT", 10)
+        bf = r.bitfield("a")
+        resp = await (
+            bf.set("u8", 8, 255)  # 00000000 11111111
+            .get("u8", 0)  # 00000000
+            .get("u4", 8)  # 1111
+            .get("u4", 12)  # 1111
+            .get("u4", 13)  # 111 0
+            .execute()
+        )
+        assert resp == [0, 0, 15, 15, 14]
+
+        # .set() returns the previous value...
+        resp = await (
+            bf.set("u8", 4, 1)  # 0000 0001
+            .get("u16", 0)  # 00000000 00011111
+            .set("u16", 0, 0)  # 00000000 00000000
+            .execute()
+        )
+        assert resp == [15, 31, 31]
+
+        # incrby adds to the value
+        resp = await (
+            bf.incrby("u8", 8, 254)  # 00000000 11111110
+            .incrby("u8", 8, 1)  # 00000000 11111111
+            .get("u16", 0)  # 00000000 11111111
+            .execute()
+        )
+        assert resp == [254, 255, 255]
+
+        # Verify overflow protection works as a method:
+        await r.delete("a")
+        resp = await (
+            bf.set("u8", 8, 254)  # 00000000 11111110
+            .overflow("fail")
+            .incrby("u8", 8, 2)  # incrby 2 would overflow, None returned
+            .incrby("u8", 8, 1)  # 00000000 11111111
+            .incrby("u8", 8, 1)  # incrby 1 would overflow, None returned
+            .get("u16", 0)  # 00000000 11111111
+            .execute()
+        )
+        assert resp == [0, None, 255, None, 255]
+
+        # Verify overflow protection works as arg to incrby:
+        await r.delete("a")
+        resp = await (
+            bf.set("u8", 8, 255)  # 00000000 11111111
+            .incrby("u8", 8, 1)  # 00000000 00000000  wrap default
+            .set("u8", 8, 255)  # 00000000 11111111
+            .incrby("u8", 8, 1, "FAIL")  # 00000000 11111111  fail
+            .incrby("u8", 8, 1)  # 00000000 11111111  still fail
+            .get("u16", 0)  # 00000000 11111111
+            .execute()
+        )
+        assert resp == [0, 0, 0, None, None, 255]
+
+        # test default default_overflow
+        await r.delete("a")
+        bf = r.bitfield("a", default_overflow="FAIL")
+        resp = await (
+            bf.set("u8", 8, 255)  # 00000000 11111111
+            .incrby("u8", 8, 1)  # 00000000 11111111  fail default
+            .get("u16", 0)  # 00000000 11111111
+            .execute()
+        )
+        assert resp == [0, None, 255]
+
+    @skip_if_server_version_lt("4.0.0")
+    async def test_memory_stats(self, r: redis.Redis):
+        # put a key into the current db to make sure that "db.<current-db>"
+        # has data
+        await r.set("foo", "bar")
+        stats = await r.memory_stats()
+        assert isinstance(stats, dict)
+        for key, value in stats.items():
+            if key.startswith("db."):
+                assert isinstance(value, dict)
+
+    @skip_if_server_version_lt("4.0.0")
+    async def test_memory_usage(self, r: redis.Redis):
+        await r.set("foo", "bar")
+        assert isinstance(await r.memory_usage("foo"), int)
+
+    @skip_if_server_version_lt("4.0.0")
+    async def test_module_list(self, r: redis.Redis):
+        assert isinstance(await r.module_list(), list)
+        assert not await r.module_list()
+
+
+class TestBinarySave:
+    async def test_binary_get_set(self, r: redis.Redis):
+        assert await r.set(" foo bar ", "123")
+        assert await r.get(" foo bar ") == b"123"
+
+        assert await r.set(" foo\r\nbar\r\n ", "456")
+        assert await r.get(" foo\r\nbar\r\n ") == b"456"
+
+        assert await r.set(" \r\n\t\x07\x13 ", "789")
+        assert await r.get(" \r\n\t\x07\x13 ") == b"789"
+
+        assert sorted(await r.keys("*")) == [
+            b" \r\n\t\x07\x13 ",
+            b" foo\r\nbar\r\n ",
+            b" foo bar ",
+        ]
+
+        assert await r.delete(" foo bar ")
+        assert await r.delete(" foo\r\nbar\r\n ")
+        assert await r.delete(" \r\n\t\x07\x13 ")
+
+    async def test_binary_lists(self, r: redis.Redis):
+        mapping = {
+            b"foo bar": [b"1", b"2", b"3"],
+            b"foo\r\nbar\r\n": [b"4", b"5", b"6"],
+            b"foo\tbar\x07": [b"7", b"8", b"9"],
+        }
+        # fill in lists
+        for key, value in mapping.items():
+            await r.rpush(key, *value)
+
+        # check that KEYS returns all the keys as they are
+        assert sorted(await r.keys("*")) == sorted(mapping.keys())
+
+        # check that it is possible to get list content by key name
+        for key, value in mapping.items():
+            assert await r.lrange(key, 0, -1) == value
+
+    async def test_22_info(self, r: redis.Redis):
+        """
+        Older Redis versions contained 'allocation_stats' in INFO that
+        was the cause of a number of bugs when parsing.
+        """
+        info = (
+            "allocation_stats:6=1,7=1,8=7141,9=180,10=92,11=116,12=5330,"
+            "13=123,14=3091,15=11048,16=225842,17=1784,18=814,19=12020,"
+            "20=2530,21=645,22=15113,23=8695,24=142860,25=318,26=3303,"
+            "27=20561,28=54042,29=37390,30=1884,31=18071,32=31367,33=160,"
+            "34=169,35=201,36=10155,37=1045,38=15078,39=22985,40=12523,"
+            "41=15588,42=265,43=1287,44=142,45=382,46=945,47=426,48=171,"
+            "49=56,50=516,51=43,52=41,53=46,54=54,55=75,56=647,57=332,"
+            "58=32,59=39,60=48,61=35,62=62,63=32,64=221,65=26,66=30,"
+            "67=36,68=41,69=44,70=26,71=144,72=169,73=24,74=37,75=25,"
+            "76=42,77=21,78=126,79=374,80=27,81=40,82=43,83=47,84=46,"
+            "85=114,86=34,87=37,88=7240,89=34,90=38,91=18,92=99,93=20,"
+            "94=18,95=17,96=15,97=22,98=18,99=69,100=17,101=22,102=15,"
+            "103=29,104=39,105=30,106=70,107=22,108=21,109=26,110=52,"
+            "111=45,112=33,113=67,114=41,115=44,116=48,117=53,118=54,"
+            "119=51,120=75,121=44,122=57,123=44,124=66,125=56,126=52,"
+            "127=81,128=108,129=70,130=50,131=51,132=53,133=45,134=62,"
+            "135=12,136=13,137=7,138=15,139=21,140=11,141=20,142=6,143=7,"
+            "144=11,145=6,146=16,147=19,148=1112,149=1,151=83,154=1,"
+            "155=1,156=1,157=1,160=1,161=1,162=2,166=1,169=1,170=1,171=2,"
+            "172=1,174=1,176=2,177=9,178=34,179=73,180=30,181=1,185=3,"
+            "187=1,188=1,189=1,192=1,196=1,198=1,200=1,201=1,204=1,205=1,"
+            "207=1,208=1,209=1,214=2,215=31,216=78,217=28,218=5,219=2,"
+            "220=1,222=1,225=1,227=1,234=1,242=1,250=1,252=1,253=1,"
+            ">=256=203"
+        )
+        parsed = parse_info(info)
+        assert "allocation_stats" in parsed
+        assert "6" in parsed["allocation_stats"]
+        assert ">=256" in parsed["allocation_stats"]
+
+    async def test_large_responses(self, r: redis.Redis):
+        """The PythonParser has some special cases for return values > 1MB"""
+        # load up 5MB of data into a key
+        data = "".join([ascii_letters] * (5000000 // len(ascii_letters)))
+        await r.set("a", data)
+        assert await r.get("a") == data.encode()
+
+    async def test_floating_point_encoding(self, r: redis.Redis):
+        """
+        High precision floating point values sent to the server should keep
+        precision.
+        """
+        timestamp = 1349673917.939762
+        await r.zadd("a", {"a1": timestamp})
+        assert await r.zscore("a", "a1") == timestamp
diff --git a/tests/test_asyncio/test_json.py b/tests/test_asyncio/test_json.py
new file mode 100644
index 00000000..6bd95810
--- /dev/null
+++ b/tests/test_asyncio/test_json.py
@@ -0,0 +1,964 @@
+import pytest
+
+import redis.asyncio as redis
+from redis import exceptions
+from redis.commands.json.path import Path
+from tests.conftest import skip_ifmodversion_lt
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.redismod
+async def test_json_setbinarykey(modclient: redis.Redis):
+    d = {"hello": "world", b"some": "value"}
+    with pytest.raises(TypeError):
+        modclient.json().set("somekey", Path.root_path(), d)
+    assert await modclient.json().set("somekey", Path.root_path(), d, decode_keys=True)
+
+
+@pytest.mark.redismod
+async def test_json_setgetdeleteforget(modclient: redis.Redis):
+    assert await modclient.json().set("foo", Path.root_path(), "bar")
+    assert await modclient.json().get("foo") == "bar"
+    assert await modclient.json().get("baz") is None
+    assert await modclient.json().delete("foo") == 1
+    assert await modclient.json().forget("foo") == 0  # second delete
+    assert await modclient.exists("foo") == 0
+
+
+@pytest.mark.redismod
+async def test_jsonget(modclient: redis.Redis):
+    await modclient.json().set("foo", Path.root_path(), "bar")
+    assert await modclient.json().get("foo") == "bar"
+
+
+@pytest.mark.redismod
+async def test_json_get_jset(modclient: redis.Redis):
+    assert await modclient.json().set("foo", Path.root_path(), "bar")
+    assert "bar" == await modclient.json().get("foo")
+    assert await modclient.json().get("baz") is None
+    assert 1 == await modclient.json().delete("foo")
+    assert await modclient.exists("foo") == 0
+
+
+@pytest.mark.redismod
+async def test_nonascii_setgetdelete(modclient: redis.Redis):
+    assert await modclient.json().set("notascii", Path.root_path(), "hyvää-élève")
+    assert "hyvää-élève" == await modclient.json().get("notascii", no_escape=True)
+    assert 1 == await modclient.json().delete("notascii")
+    assert await modclient.exists("notascii") == 0
+
+
+@pytest.mark.redismod
+async def test_jsonsetexistentialmodifiersshouldsucceed(modclient: redis.Redis):
+    obj = {"foo": "bar"}
+    assert await modclient.json().set("obj", Path.root_path(), obj)
+
+    # Test that flags prevent updates when conditions are unmet
+    assert await modclient.json().set("obj", Path("foo"), "baz", nx=True) is None
+    assert await modclient.json().set("obj", Path("qaz"), "baz", xx=True) is None
+
+    # Test that flags allow updates when conditions are met
+    assert await modclient.json().set("obj", Path("foo"), "baz", xx=True)
+    assert await modclient.json().set("obj", Path("qaz"), "baz", nx=True)
+
+    # Test that flags are mutually exlusive
+    with pytest.raises(Exception):
+        await modclient.json().set("obj", Path("foo"), "baz", nx=True, xx=True)
+
+
+@pytest.mark.redismod
+async def test_mgetshouldsucceed(modclient: redis.Redis):
+    await modclient.json().set("1", Path.root_path(), 1)
+    await modclient.json().set("2", Path.root_path(), 2)
+    assert await modclient.json().mget(["1"], Path.root_path()) == [1]
+
+    assert await modclient.json().mget([1, 2], Path.root_path()) == [1, 2]
+
+
+@pytest.mark.redismod
+@skip_ifmodversion_lt("99.99.99", "ReJSON")  # todo: update after the release
+async def test_clear(modclient: redis.Redis):
+    await modclient.json().set("arr", Path.root_path(), [0, 1, 2, 3, 4])
+    assert 1 == await modclient.json().clear("arr", Path.root_path())
+    assert [] == await modclient.json().get("arr")
+
+
+@pytest.mark.redismod
+async def test_type(modclient: redis.Redis):
+    await modclient.json().set("1", Path.root_path(), 1)
+    assert "integer" == await modclient.json().type("1", Path.root_path())
+    assert "integer" == await modclient.json().type("1")
+
+
+@pytest.mark.redismod
+async def test_numincrby(modclient):
+    await modclient.json().set("num", Path.root_path(), 1)
+    assert 2 == await modclient.json().numincrby("num", Path.root_path(), 1)
+    assert 2.5 == await modclient.json().numincrby("num", Path.root_path(), 0.5)
+    assert 1.25 == await modclient.json().numincrby("num", Path.root_path(), -1.25)
+
+
+@pytest.mark.redismod
+async def test_nummultby(modclient: redis.Redis):
+    await modclient.json().set("num", Path.root_path(), 1)
+
+    with pytest.deprecated_call():
+        assert 2 == await modclient.json().nummultby("num", Path.root_path(), 2)
+        assert 5 == await modclient.json().nummultby("num", Path.root_path(), 2.5)
+        assert 2.5 == await modclient.json().nummultby("num", Path.root_path(), 0.5)
+
+
+@pytest.mark.redismod
+@skip_ifmodversion_lt("99.99.99", "ReJSON")  # todo: update after the release
+async def test_toggle(modclient: redis.Redis):
+    await modclient.json().set("bool", Path.root_path(), False)
+    assert await modclient.json().toggle("bool", Path.root_path())
+    assert await modclient.json().toggle("bool", Path.root_path()) is False
+    # check non-boolean value
+    await modclient.json().set("num", Path.root_path(), 1)
+    with pytest.raises(exceptions.ResponseError):
+        await modclient.json().toggle("num", Path.root_path())
+
+
+@pytest.mark.redismod
+async def test_strappend(modclient: redis.Redis):
+    await modclient.json().set("jsonkey", Path.root_path(), "foo")
+    assert 6 == await modclient.json().strappend("jsonkey", "bar")
+    assert "foobar" == await modclient.json().get("jsonkey", Path.root_path())
+
+
+@pytest.mark.redismod
+async def test_strlen(modclient: redis.Redis):
+    await modclient.json().set("str", Path.root_path(), "foo")
+    assert 3 == await modclient.json().strlen("str", Path.root_path())
+    await modclient.json().strappend("str", "bar", Path.root_path())
+    assert 6 == await modclient.json().strlen("str", Path.root_path())
+    assert 6 == await modclient.json().strlen("str")
+
+
+@pytest.mark.redismod
+async def test_arrappend(modclient: redis.Redis):
+    await modclient.json().set("arr", Path.root_path(), [1])
+    assert 2 == await modclient.json().arrappend("arr", Path.root_path(), 2)
+    assert 4 == await modclient.json().arrappend("arr", Path.root_path(), 3, 4)
+    assert 7 == await modclient.json().arrappend("arr", Path.root_path(), *[5, 6, 7])
+
+
+@pytest.mark.redismod
+async def test_arrindex(modclient: redis.Redis):
+    await modclient.json().set("arr", Path.root_path(), [0, 1, 2, 3, 4])
+    assert 1 == await modclient.json().arrindex("arr", Path.root_path(), 1)
+    assert -1 == await modclient.json().arrindex("arr", Path.root_path(), 1, 2)
+
+
+@pytest.mark.redismod
+async def test_arrinsert(modclient: redis.Redis):
+    await modclient.json().set("arr", Path.root_path(), [0, 4])
+    assert 5 - -await modclient.json().arrinsert(
+        "arr",
+        Path.root_path(),
+        1,
+        *[
+            1,
+            2,
+            3,
+        ],
+    )
+    assert [0, 1, 2, 3, 4] == await modclient.json().get("arr")
+
+    # test prepends
+    await modclient.json().set("val2", Path.root_path(), [5, 6, 7, 8, 9])
+    await modclient.json().arrinsert("val2", Path.root_path(), 0, ["some", "thing"])
+    assert await modclient.json().get("val2") == [["some", "thing"], 5, 6, 7, 8, 9]
+
+
+@pytest.mark.redismod
+async def test_arrlen(modclient: redis.Redis):
+    await modclient.json().set("arr", Path.root_path(), [0, 1, 2, 3, 4])
+    assert 5 == await modclient.json().arrlen("arr", Path.root_path())
+    assert 5 == await modclient.json().arrlen("arr")
+    assert await modclient.json().arrlen("fakekey") is None
+
+
+@pytest.mark.redismod
+async def test_arrpop(modclient: redis.Redis):
+    await modclient.json().set("arr", Path.root_path(), [0, 1, 2, 3, 4])
+    assert 4 == await modclient.json().arrpop("arr", Path.root_path(), 4)
+    assert 3 == await modclient.json().arrpop("arr", Path.root_path(), -1)
+    assert 2 == await modclient.json().arrpop("arr", Path.root_path())
+    assert 0 == await modclient.json().arrpop("arr", Path.root_path(), 0)
+    assert [1] == await modclient.json().get("arr")
+
+    # test out of bounds
+    await modclient.json().set("arr", Path.root_path(), [0, 1, 2, 3, 4])
+    assert 4 == await modclient.json().arrpop("arr", Path.root_path(), 99)
+
+    # none test
+    await modclient.json().set("arr", Path.root_path(), [])
+    assert await modclient.json().arrpop("arr") is None
+
+
+@pytest.mark.redismod
+async def test_arrtrim(modclient: redis.Redis):
+    await modclient.json().set("arr", Path.root_path(), [0, 1, 2, 3, 4])
+    assert 3 == await modclient.json().arrtrim("arr", Path.root_path(), 1, 3)
+    assert [1, 2, 3] == await modclient.json().get("arr")
+
+    # <0 test, should be 0 equivalent
+    await modclient.json().set("arr", Path.root_path(), [0, 1, 2, 3, 4])
+    assert 0 == await modclient.json().arrtrim("arr", Path.root_path(), -1, 3)
+
+    # testing stop > end
+    await modclient.json().set("arr", Path.root_path(), [0, 1, 2, 3, 4])
+    assert 2 == await modclient.json().arrtrim("arr", Path.root_path(), 3, 99)
+
+    # start > array size and stop
+    await modclient.json().set("arr", Path.root_path(), [0, 1, 2, 3, 4])
+    assert 0 == await modclient.json().arrtrim("arr", Path.root_path(), 9, 1)
+
+    # all larger
+    await modclient.json().set("arr", Path.root_path(), [0, 1, 2, 3, 4])
+    assert 0 == await modclient.json().arrtrim("arr", Path.root_path(), 9, 11)
+
+
+@pytest.mark.redismod
+async def test_resp(modclient: redis.Redis):
+    obj = {"foo": "bar", "baz": 1, "qaz": True}
+    await modclient.json().set("obj", Path.root_path(), obj)
+    assert "bar" == await modclient.json().resp("obj", Path("foo"))
+    assert 1 == await modclient.json().resp("obj", Path("baz"))
+    assert await modclient.json().resp("obj", Path("qaz"))
+    assert isinstance(await modclient.json().resp("obj"), list)
+
+
+@pytest.mark.redismod
+async def test_objkeys(modclient: redis.Redis):
+    obj = {"foo": "bar", "baz": "qaz"}
+    await modclient.json().set("obj", Path.root_path(), obj)
+    keys = await modclient.json().objkeys("obj", Path.root_path())
+    keys.sort()
+    exp = list(obj.keys())
+    exp.sort()
+    assert exp == keys
+
+    await modclient.json().set("obj", Path.root_path(), obj)
+    keys = await modclient.json().objkeys("obj")
+    assert keys == list(obj.keys())
+
+    assert await modclient.json().objkeys("fakekey") is None
+
+
+@pytest.mark.redismod
+async def test_objlen(modclient: redis.Redis):
+    obj = {"foo": "bar", "baz": "qaz"}
+    await modclient.json().set("obj", Path.root_path(), obj)
+    assert len(obj) == await modclient.json().objlen("obj", Path.root_path())
+
+    await modclient.json().set("obj", Path.root_path(), obj)
+    assert len(obj) == await modclient.json().objlen("obj")
+
+
+# @pytest.mark.redismod
+# async def test_json_commands_in_pipeline(modclient: redis.Redis):
+#     async with modclient.json().pipeline() as p:
+#         p.set("foo", Path.root_path(), "bar")
+#         p.get("foo")
+#         p.delete("foo")
+#         assert [True, "bar", 1] == await p.execute()
+#     assert await modclient.keys() == []
+#     assert await modclient.get("foo") is None
+
+#     # now with a true, json object
+#     await modclient.flushdb()
+#     p = await modclient.json().pipeline()
+#     d = {"hello": "world", "oh": "snap"}
+#     with pytest.deprecated_call():
+#         p.jsonset("foo", Path.root_path(), d)
+#         p.jsonget("foo")
+#     p.exists("notarealkey")
+#     p.delete("foo")
+#     assert [True, d, 0, 1] == p.execute()
+#     assert await modclient.keys() == []
+#     assert await modclient.get("foo") is None
+
+
+@pytest.mark.redismod
+async def test_json_delete_with_dollar(modclient: redis.Redis):
+    doc1 = {"a": 1, "nested": {"a": 2, "b": 3}}
+    assert await modclient.json().set("doc1", "$", doc1)
+    assert await modclient.json().delete("doc1", "$..a") == 2
+    r = await modclient.json().get("doc1", "$")
+    assert r == [{"nested": {"b": 3}}]
+
+    doc2 = {"a": {"a": 2, "b": 3}, "b": ["a", "b"], "nested": {"b": [True, "a", "b"]}}
+    assert await modclient.json().set("doc2", "$", doc2)
+    assert await modclient.json().delete("doc2", "$..a") == 1
+    res = await modclient.json().get("doc2", "$")
+    assert res == [{"nested": {"b": [True, "a", "b"]}, "b": ["a", "b"]}]
+
+    doc3 = [
+        {
+            "ciao": ["non ancora"],
+            "nested": [
+                {"ciao": [1, "a"]},
+                {"ciao": [2, "a"]},
+                {"ciaoc": [3, "non", "ciao"]},
+                {"ciao": [4, "a"]},
+                {"e": [5, "non", "ciao"]},
+            ],
+        }
+    ]
+    assert await modclient.json().set("doc3", "$", doc3)
+    assert await modclient.json().delete("doc3", '$.[0]["nested"]..ciao') == 3
+
+    doc3val = [
+        [
+            {
+                "ciao": ["non ancora"],
+                "nested": [
+                    {},
+                    {},
+                    {"ciaoc": [3, "non", "ciao"]},
+                    {},
+                    {"e": [5, "non", "ciao"]},
+                ],
+            }
+        ]
+    ]
+    res = await modclient.json().get("doc3", "$")
+    assert res == doc3val
+
+    # Test async default path
+    assert await modclient.json().delete("doc3") == 1
+    assert await modclient.json().get("doc3", "$") is None
+
+    await modclient.json().delete("not_a_document", "..a")
+
+
+@pytest.mark.redismod
+async def test_json_forget_with_dollar(modclient: redis.Redis):
+    doc1 = {"a": 1, "nested": {"a": 2, "b": 3}}
+    assert await modclient.json().set("doc1", "$", doc1)
+    assert await modclient.json().forget("doc1", "$..a") == 2
+    r = await modclient.json().get("doc1", "$")
+    assert r == [{"nested": {"b": 3}}]
+
+    doc2 = {"a": {"a": 2, "b": 3}, "b": ["a", "b"], "nested": {"b": [True, "a", "b"]}}
+    assert await modclient.json().set("doc2", "$", doc2)
+    assert await modclient.json().forget("doc2", "$..a") == 1
+    res = await modclient.json().get("doc2", "$")
+    assert res == [{"nested": {"b": [True, "a", "b"]}, "b": ["a", "b"]}]
+
+    doc3 = [
+        {
+            "ciao": ["non ancora"],
+            "nested": [
+                {"ciao": [1, "a"]},
+                {"ciao": [2, "a"]},
+                {"ciaoc": [3, "non", "ciao"]},
+                {"ciao": [4, "a"]},
+                {"e": [5, "non", "ciao"]},
+            ],
+        }
+    ]
+    assert await modclient.json().set("doc3", "$", doc3)
+    assert await modclient.json().forget("doc3", '$.[0]["nested"]..ciao') == 3
+
+    doc3val = [
+        [
+            {
+                "ciao": ["non ancora"],
+                "nested": [
+                    {},
+                    {},
+                    {"ciaoc": [3, "non", "ciao"]},
+                    {},
+                    {"e": [5, "non", "ciao"]},
+                ],
+            }
+        ]
+    ]
+    res = await modclient.json().get("doc3", "$")
+    assert res == doc3val
+
+    # Test async default path
+    assert await modclient.json().forget("doc3") == 1
+    assert await modclient.json().get("doc3", "$") is None
+
+    await modclient.json().forget("not_a_document", "..a")
+
+
+@pytest.mark.redismod
+async def test_json_mget_dollar(modclient: redis.Redis):
+    # Test mget with multi paths
+    await modclient.json().set(
+        "doc1",
+        "$",
+        {"a": 1, "b": 2, "nested": {"a": 3}, "c": None, "nested2": {"a": None}},
+    )
+    await modclient.json().set(
+        "doc2",
+        "$",
+        {"a": 4, "b": 5, "nested": {"a": 6}, "c": None, "nested2": {"a": [None]}},
+    )
+    # Compare also to single JSON.GET
+    assert await modclient.json().get("doc1", "$..a") == [1, 3, None]
+    assert await modclient.json().get("doc2", "$..a") == [4, 6, [None]]
+
+    # Test mget with single path
+    await modclient.json().mget("doc1", "$..a") == [1, 3, None]
+    # Test mget with multi path
+    res = await modclient.json().mget(["doc1", "doc2"], "$..a")
+    assert res == [[1, 3, None], [4, 6, [None]]]
+
+    # Test missing key
+    res = await modclient.json().mget(["doc1", "missing_doc"], "$..a")
+    assert res == [[1, 3, None], None]
+    res = await modclient.json().mget(["missing_doc1", "missing_doc2"], "$..a")
+    assert res == [None, None]
+
+
+@pytest.mark.redismod
+async def test_numby_commands_dollar(modclient: redis.Redis):
+
+    # Test NUMINCRBY
+    await modclient.json().set(
+        "doc1", "$", {"a": "b", "b": [{"a": 2}, {"a": 5.0}, {"a": "c"}]}
+    )
+    # Test multi
+    assert await modclient.json().numincrby("doc1", "$..a", 2) == [None, 4, 7.0, None]
+
+    res = await modclient.json().numincrby("doc1", "$..a", 2.5)
+    assert res == [None, 6.5, 9.5, None]
+    # Test single
+    assert await modclient.json().numincrby("doc1", "$.b[1].a", 2) == [11.5]
+
+    assert await modclient.json().numincrby("doc1", "$.b[2].a", 2) == [None]
+    assert await modclient.json().numincrby("doc1", "$.b[1].a", 3.5) == [15.0]
+
+    # Test NUMMULTBY
+    await modclient.json().set(
+        "doc1", "$", {"a": "b", "b": [{"a": 2}, {"a": 5.0}, {"a": "c"}]}
+    )
+
+    # test list
+    with pytest.deprecated_call():
+        res = await modclient.json().nummultby("doc1", "$..a", 2)
+        assert res == [None, 4, 10, None]
+        res = await modclient.json().nummultby("doc1", "$..a", 2.5)
+        assert res == [None, 10.0, 25.0, None]
+
+    # Test single
+    with pytest.deprecated_call():
+        assert await modclient.json().nummultby("doc1", "$.b[1].a", 2) == [50.0]
+        assert await modclient.json().nummultby("doc1", "$.b[2].a", 2) == [None]
+        assert await modclient.json().nummultby("doc1", "$.b[1].a", 3) == [150.0]
+
+    # test missing keys
+    with pytest.raises(exceptions.ResponseError):
+        await modclient.json().numincrby("non_existing_doc", "$..a", 2)
+        await modclient.json().nummultby("non_existing_doc", "$..a", 2)
+
+    # Test legacy NUMINCRBY
+    await modclient.json().set(
+        "doc1", "$", {"a": "b", "b": [{"a": 2}, {"a": 5.0}, {"a": "c"}]}
+    )
+    await modclient.json().numincrby("doc1", ".b[0].a", 3) == 5
+
+    # Test legacy NUMMULTBY
+    await modclient.json().set(
+        "doc1", "$", {"a": "b", "b": [{"a": 2}, {"a": 5.0}, {"a": "c"}]}
+    )
+
+    with pytest.deprecated_call():
+        await modclient.json().nummultby("doc1", ".b[0].a", 3) == 6
+
+
+@pytest.mark.redismod
+async def test_strappend_dollar(modclient: redis.Redis):
+
+    await modclient.json().set(
+        "doc1", "$", {"a": "foo", "nested1": {"a": "hello"}, "nested2": {"a": 31}}
+    )
+    # Test multi
+    await modclient.json().strappend("doc1", "bar", "$..a") == [6, 8, None]
+
+    await modclient.json().get("doc1", "$") == [
+        {"a": "foobar", "nested1": {"a": "hellobar"}, "nested2": {"a": 31}}
+    ]
+    # Test single
+    await modclient.json().strappend("doc1", "baz", "$.nested1.a") == [11]
+
+    await modclient.json().get("doc1", "$") == [
+        {"a": "foobar", "nested1": {"a": "hellobarbaz"}, "nested2": {"a": 31}}
+    ]
+
+    # Test missing key
+    with pytest.raises(exceptions.ResponseError):
+        await modclient.json().strappend("non_existing_doc", "$..a", "err")
+
+    # Test multi
+    await modclient.json().strappend("doc1", "bar", ".*.a") == 8
+    await modclient.json().get("doc1", "$") == [
+        {"a": "foo", "nested1": {"a": "hellobar"}, "nested2": {"a": 31}}
+    ]
+
+    # Test missing path
+    with pytest.raises(exceptions.ResponseError):
+        await modclient.json().strappend("doc1", "piu")
+
+
+@pytest.mark.redismod
+async def test_strlen_dollar(modclient: redis.Redis):
+
+    # Test multi
+    await modclient.json().set(
+        "doc1", "$", {"a": "foo", "nested1": {"a": "hello"}, "nested2": {"a": 31}}
+    )
+    assert await modclient.json().strlen("doc1", "$..a") == [3, 5, None]
+
+    res2 = await modclient.json().strappend("doc1", "bar", "$..a")
+    res1 = await modclient.json().strlen("doc1", "$..a")
+    assert res1 == res2
+
+    # Test single
+    await modclient.json().strlen("doc1", "$.nested1.a") == [8]
+    await modclient.json().strlen("doc1", "$.nested2.a") == [None]
+
+    # Test missing key
+    with pytest.raises(exceptions.ResponseError):
+        await modclient.json().strlen("non_existing_doc", "$..a")
+
+
+@pytest.mark.redismod
+async def test_arrappend_dollar(modclient: redis.Redis):
+    await modclient.json().set(
+        "doc1",
+        "$",
+        {
+            "a": ["foo"],
+            "nested1": {"a": ["hello", None, "world"]},
+            "nested2": {"a": 31},
+        },
+    )
+    # Test multi
+    await modclient.json().arrappend("doc1", "$..a", "bar", "racuda") == [3, 5, None]
+    assert await modclient.json().get("doc1", "$") == [
+        {
+            "a": ["foo", "bar", "racuda"],
+            "nested1": {"a": ["hello", None, "world", "bar", "racuda"]},
+            "nested2": {"a": 31},
+        }
+    ]
+
+    # Test single
+    assert await modclient.json().arrappend("doc1", "$.nested1.a", "baz") == [6]
+    assert await modclient.json().get("doc1", "$") == [
+        {
+            "a": ["foo", "bar", "racuda"],
+            "nested1": {"a": ["hello", None, "world", "bar", "racuda", "baz"]},
+            "nested2": {"a": 31},
+        }
+    ]
+
+    # Test missing key
+    with pytest.raises(exceptions.ResponseError):
+        await modclient.json().arrappend("non_existing_doc", "$..a")
+
+    # Test legacy
+    await modclient.json().set(
+        "doc1",
+        "$",
+        {
+            "a": ["foo"],
+            "nested1": {"a": ["hello", None, "world"]},
+            "nested2": {"a": 31},
+        },
+    )
+    # Test multi (all paths are updated, but return result of last path)
+    assert await modclient.json().arrappend("doc1", "..a", "bar", "racuda") == 5
+
+    assert await modclient.json().get("doc1", "$") == [
+        {
+            "a": ["foo", "bar", "racuda"],
+            "nested1": {"a": ["hello", None, "world", "bar", "racuda"]},
+            "nested2": {"a": 31},
+        }
+    ]
+    # Test single
+    assert await modclient.json().arrappend("doc1", ".nested1.a", "baz") == 6
+    assert await modclient.json().get("doc1", "$") == [
+        {
+            "a": ["foo", "bar", "racuda"],
+            "nested1": {"a": ["hello", None, "world", "bar", "racuda", "baz"]},
+            "nested2": {"a": 31},
+        }
+    ]
+
+    # Test missing key
+    with pytest.raises(exceptions.ResponseError):
+        await modclient.json().arrappend("non_existing_doc", "$..a")
+
+
+@pytest.mark.redismod
+async def test_arrinsert_dollar(modclient: redis.Redis):
+    await modclient.json().set(
+        "doc1",
+        "$",
+        {
+            "a": ["foo"],
+            "nested1": {"a": ["hello", None, "world"]},
+            "nested2": {"a": 31},
+        },
+    )
+    # Test multi
+    res = await modclient.json().arrinsert("doc1", "$..a", "1", "bar", "racuda")
+    assert res == [3, 5, None]
+
+    assert await modclient.json().get("doc1", "$") == [
+        {
+            "a": ["foo", "bar", "racuda"],
+            "nested1": {"a": ["hello", "bar", "racuda", None, "world"]},
+            "nested2": {"a": 31},
+        }
+    ]
+    # Test single
+    assert await modclient.json().arrinsert("doc1", "$.nested1.a", -2, "baz") == [6]
+    assert await modclient.json().get("doc1", "$") == [
+        {
+            "a": ["foo", "bar", "racuda"],
+            "nested1": {"a": ["hello", "bar", "racuda", "baz", None, "world"]},
+            "nested2": {"a": 31},
+        }
+    ]
+
+    # Test missing key
+    with pytest.raises(exceptions.ResponseError):
+        await modclient.json().arrappend("non_existing_doc", "$..a")
+
+
+@pytest.mark.redismod
+async def test_arrlen_dollar(modclient: redis.Redis):
+
+    await modclient.json().set(
+        "doc1",
+        "$",
+        {
+            "a": ["foo"],
+            "nested1": {"a": ["hello", None, "world"]},
+            "nested2": {"a": 31},
+        },
+    )
+
+    # Test multi
+    assert await modclient.json().arrlen("doc1", "$..a") == [1, 3, None]
+    res = await modclient.json().arrappend("doc1", "$..a", "non", "abba", "stanza")
+    assert res == [4, 6, None]
+
+    await modclient.json().clear("doc1", "$.a")
+    assert await modclient.json().arrlen("doc1", "$..a") == [0, 6, None]
+    # Test single
+    assert await modclient.json().arrlen("doc1", "$.nested1.a") == [6]
+
+    # Test missing key
+    with pytest.raises(exceptions.ResponseError):
+        await modclient.json().arrappend("non_existing_doc", "$..a")
+
+    await modclient.json().set(
+        "doc1",
+        "$",
+        {
+            "a": ["foo"],
+            "nested1": {"a": ["hello", None, "world"]},
+            "nested2": {"a": 31},
+        },
+    )
+    # Test multi (return result of last path)
+    assert await modclient.json().arrlen("doc1", "$..a") == [1, 3, None]
+    assert await modclient.json().arrappend("doc1", "..a", "non", "abba", "stanza") == 6
+
+    # Test single
+    assert await modclient.json().arrlen("doc1", ".nested1.a") == 6
+
+    # Test missing key
+    assert await modclient.json().arrlen("non_existing_doc", "..a") is None
+
+
+@pytest.mark.redismod
+async def test_arrpop_dollar(modclient: redis.Redis):
+    await modclient.json().set(
+        "doc1",
+        "$",
+        {
+            "a": ["foo"],
+            "nested1": {"a": ["hello", None, "world"]},
+            "nested2": {"a": 31},
+        },
+    )
+
+    # # # Test multi
+    assert await modclient.json().arrpop("doc1", "$..a", 1) == ['"foo"', None, None]
+
+    assert await modclient.json().get("doc1", "$") == [
+        {"a": [], "nested1": {"a": ["hello", "world"]}, "nested2": {"a": 31}}
+    ]
+
+    # Test missing key
+    with pytest.raises(exceptions.ResponseError):
+        await modclient.json().arrpop("non_existing_doc", "..a")
+
+    # # Test legacy
+    await modclient.json().set(
+        "doc1",
+        "$",
+        {
+            "a": ["foo"],
+            "nested1": {"a": ["hello", None, "world"]},
+            "nested2": {"a": 31},
+        },
+    )
+    # Test multi (all paths are updated, but return result of last path)
+    await modclient.json().arrpop("doc1", "..a", "1") is None
+    assert await modclient.json().get("doc1", "$") == [
+        {"a": [], "nested1": {"a": ["hello", "world"]}, "nested2": {"a": 31}}
+    ]
+
+    # # Test missing key
+    with pytest.raises(exceptions.ResponseError):
+        await modclient.json().arrpop("non_existing_doc", "..a")
+
+
+@pytest.mark.redismod
+async def test_arrtrim_dollar(modclient: redis.Redis):
+
+    await modclient.json().set(
+        "doc1",
+        "$",
+        {
+            "a": ["foo"],
+            "nested1": {"a": ["hello", None, "world"]},
+            "nested2": {"a": 31},
+        },
+    )
+    # Test multi
+    assert await modclient.json().arrtrim("doc1", "$..a", "1", -1) == [0, 2, None]
+    assert await modclient.json().get("doc1", "$") == [
+        {"a": [], "nested1": {"a": [None, "world"]}, "nested2": {"a": 31}}
+    ]
+
+    assert await modclient.json().arrtrim("doc1", "$..a", "1", "1") == [0, 1, None]
+    assert await modclient.json().get("doc1", "$") == [
+        {"a": [], "nested1": {"a": ["world"]}, "nested2": {"a": 31}}
+    ]
+    # Test single
+    assert await modclient.json().arrtrim("doc1", "$.nested1.a", 1, 0) == [0]
+    assert await modclient.json().get("doc1", "$") == [
+        {"a": [], "nested1": {"a": []}, "nested2": {"a": 31}}
+    ]
+
+    # Test missing key
+    with pytest.raises(exceptions.ResponseError):
+        await modclient.json().arrtrim("non_existing_doc", "..a", "0", 1)
+
+    # Test legacy
+    await modclient.json().set(
+        "doc1",
+        "$",
+        {
+            "a": ["foo"],
+            "nested1": {"a": ["hello", None, "world"]},
+            "nested2": {"a": 31},
+        },
+    )
+
+    # Test multi (all paths are updated, but return result of last path)
+    assert await modclient.json().arrtrim("doc1", "..a", "1", "-1") == 2
+
+    # Test single
+    assert await modclient.json().arrtrim("doc1", ".nested1.a", "1", "1") == 1
+    assert await modclient.json().get("doc1", "$") == [
+        {"a": [], "nested1": {"a": ["world"]}, "nested2": {"a": 31}}
+    ]
+
+    # Test missing key
+    with pytest.raises(exceptions.ResponseError):
+        await modclient.json().arrtrim("non_existing_doc", "..a", 1, 1)
+
+
+@pytest.mark.redismod
+async def test_objkeys_dollar(modclient: redis.Redis):
+    await modclient.json().set(
+        "doc1",
+        "$",
+        {
+            "nested1": {"a": {"foo": 10, "bar": 20}},
+            "a": ["foo"],
+            "nested2": {"a": {"baz": 50}},
+        },
+    )
+
+    # Test single
+    assert await modclient.json().objkeys("doc1", "$.nested1.a") == [["foo", "bar"]]
+
+    # Test legacy
+    assert await modclient.json().objkeys("doc1", ".*.a") == ["foo", "bar"]
+    # Test single
+    assert await modclient.json().objkeys("doc1", ".nested2.a") == ["baz"]
+
+    # Test missing key
+    assert await modclient.json().objkeys("non_existing_doc", "..a") is None
+
+    # Test non existing doc
+    with pytest.raises(exceptions.ResponseError):
+        assert await modclient.json().objkeys("non_existing_doc", "$..a") == []
+
+    assert await modclient.json().objkeys("doc1", "$..nowhere") == []
+
+
+@pytest.mark.redismod
+async def test_objlen_dollar(modclient: redis.Redis):
+    await modclient.json().set(
+        "doc1",
+        "$",
+        {
+            "nested1": {"a": {"foo": 10, "bar": 20}},
+            "a": ["foo"],
+            "nested2": {"a": {"baz": 50}},
+        },
+    )
+    # Test multi
+    assert await modclient.json().objlen("doc1", "$..a") == [2, None, 1]
+    # Test single
+    assert await modclient.json().objlen("doc1", "$.nested1.a") == [2]
+
+    # Test missing key, and path
+    with pytest.raises(exceptions.ResponseError):
+        await modclient.json().objlen("non_existing_doc", "$..a")
+
+    assert await modclient.json().objlen("doc1", "$.nowhere") == []
+
+    # Test legacy
+    assert await modclient.json().objlen("doc1", ".*.a") == 2
+
+    # Test single
+    assert await modclient.json().objlen("doc1", ".nested2.a") == 1
+
+    # Test missing key
+    assert await modclient.json().objlen("non_existing_doc", "..a") is None
+
+    # Test missing path
+    # with pytest.raises(exceptions.ResponseError):
+    await modclient.json().objlen("doc1", ".nowhere")
+
+
+@pytest.mark.redismod
+def load_types_data(nested_key_name):
+    td = {
+        "object": {},
+        "array": [],
+        "string": "str",
+        "integer": 42,
+        "number": 1.2,
+        "boolean": False,
+        "null": None,
+    }
+    jdata = {}
+    types = []
+    for i, (k, v) in zip(range(1, len(td) + 1), iter(td.items())):
+        jdata["nested" + str(i)] = {nested_key_name: v}
+        types.append(k)
+
+    return jdata, types
+
+
+@pytest.mark.redismod
+async def test_type_dollar(modclient: redis.Redis):
+    jdata, jtypes = load_types_data("a")
+    await modclient.json().set("doc1", "$", jdata)
+    # Test multi
+    assert await modclient.json().type("doc1", "$..a") == jtypes
+
+    # Test single
+    assert await modclient.json().type("doc1", "$.nested2.a") == [jtypes[1]]
+
+    # Test missing key
+    assert await modclient.json().type("non_existing_doc", "..a") is None
+
+
+@pytest.mark.redismod
+async def test_clear_dollar(modclient: redis.Redis):
+
+    await modclient.json().set(
+        "doc1",
+        "$",
+        {
+            "nested1": {"a": {"foo": 10, "bar": 20}},
+            "a": ["foo"],
+            "nested2": {"a": "claro"},
+            "nested3": {"a": {"baz": 50}},
+        },
+    )
+    # Test multi
+    assert await modclient.json().clear("doc1", "$..a") == 4
+
+    assert await modclient.json().get("doc1", "$") == [
+        {"nested1": {"a": {}}, "a": [], "nested2": {"a": ""}, "nested3": {"a": {}}}
+    ]
+
+    # Test single
+    await modclient.json().set(
+        "doc1",
+        "$",
+        {
+            "nested1": {"a": {"foo": 10, "bar": 20}},
+            "a": ["foo"],
+            "nested2": {"a": "claro"},
+            "nested3": {"a": {"baz": 50}},
+        },
+    )
+    assert await modclient.json().clear("doc1", "$.nested1.a") == 1
+    assert await modclient.json().get("doc1", "$") == [
+        {
+            "nested1": {"a": {}},
+            "a": ["foo"],
+            "nested2": {"a": "claro"},
+            "nested3": {"a": {"baz": 50}},
+        }
+    ]
+
+    # Test missing path (async defaults to root)
+    assert await modclient.json().clear("doc1") == 1
+    assert await modclient.json().get("doc1", "$") == [{}]
+
+    # Test missing key
+    with pytest.raises(exceptions.ResponseError):
+        await modclient.json().clear("non_existing_doc", "$..a")
+
+
+@pytest.mark.redismod
+async def test_toggle_dollar(modclient: redis.Redis):
+    await modclient.json().set(
+        "doc1",
+        "$",
+        {
+            "a": ["foo"],
+            "nested1": {"a": False},
+            "nested2": {"a": 31},
+            "nested3": {"a": True},
+        },
+    )
+    # Test multi
+    assert await modclient.json().toggle("doc1", "$..a") == [None, 1, None, 0]
+    assert await modclient.json().get("doc1", "$") == [
+        {
+            "a": ["foo"],
+            "nested1": {"a": True},
+            "nested2": {"a": 31},
+            "nested3": {"a": False},
+        }
+    ]
+
+    # Test missing key
+    with pytest.raises(exceptions.ResponseError):
+        await modclient.json().toggle("non_existing_doc", "$..a")
diff --git a/tests/test_asyncio/test_timeseries.py b/tests/test_asyncio/test_timeseries.py
new file mode 100644
index 00000000..70ca6fde
--- /dev/null
+++ b/tests/test_asyncio/test_timeseries.py
@@ -0,0 +1,556 @@
+import time
+from time import sleep
+
+import pytest
+
+import redis.asyncio as redis
+from tests.conftest import skip_ifmodversion_lt
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.redismod
+async def test_create(modclient: redis.Redis):
+    assert await modclient.ts().create(1)
+    assert await modclient.ts().create(2, retention_msecs=5)
+    assert await modclient.ts().create(3, labels={"Redis": "Labs"})
+    assert await modclient.ts().create(4, retention_msecs=20, labels={"Time": "Series"})
+    info = await modclient.ts().info(4)
+    assert 20 == info.retention_msecs
+    assert "Series" == info.labels["Time"]
+
+    # Test for a chunk size of 128 Bytes
+    assert await modclient.ts().create("time-serie-1", chunk_size=128)
+    info = await modclient.ts().info("time-serie-1")
+    assert 128, info.chunk_size
+
+
+@pytest.mark.redismod
+@skip_ifmodversion_lt("1.4.0", "timeseries")
+async def test_create_duplicate_policy(modclient: redis.Redis):
+    # Test for duplicate policy
+    for duplicate_policy in ["block", "last", "first", "min", "max"]:
+        ts_name = f"time-serie-ooo-{duplicate_policy}"
+        assert await modclient.ts().create(ts_name, duplicate_policy=duplicate_policy)
+        info = await modclient.ts().info(ts_name)
+        assert duplicate_policy == info.duplicate_policy
+
+
+@pytest.mark.redismod
+async def test_alter(modclient: redis.Redis):
+    assert await modclient.ts().create(1)
+    res = await modclient.ts().info(1)
+    assert 0 == res.retention_msecs
+    assert await modclient.ts().alter(1, retention_msecs=10)
+    res = await modclient.ts().info(1)
+    assert {} == res.labels
+    res = await modclient.ts().info(1)
+    assert 10 == res.retention_msecs
+    assert await modclient.ts().alter(1, labels={"Time": "Series"})
+    res = await modclient.ts().info(1)
+    assert "Series" == res.labels["Time"]
+    res = await modclient.ts().info(1)
+    assert 10 == res.retention_msecs
+
+
+@pytest.mark.redismod
+@skip_ifmodversion_lt("1.4.0", "timeseries")
+async def test_alter_diplicate_policy(modclient: redis.Redis):
+    assert await modclient.ts().create(1)
+    info = await modclient.ts().info(1)
+    assert info.duplicate_policy is None
+    assert await modclient.ts().alter(1, duplicate_policy="min")
+    info = await modclient.ts().info(1)
+    assert "min" == info.duplicate_policy
+
+
+@pytest.mark.redismod
+async def test_add(modclient: redis.Redis):
+    assert 1 == await modclient.ts().add(1, 1, 1)
+    assert 2 == await modclient.ts().add(2, 2, 3, retention_msecs=10)
+    assert 3 == await modclient.ts().add(3, 3, 2, labels={"Redis": "Labs"})
+    assert 4 == await modclient.ts().add(
+        4, 4, 2, retention_msecs=10, labels={"Redis": "Labs", "Time": "Series"}
+    )
+    res = await modclient.ts().add(5, "*", 1)
+    assert round(time.time()) == round(float(res) / 1000)
+
+    info = await modclient.ts().info(4)
+    assert 10 == info.retention_msecs
+    assert "Labs" == info.labels["Redis"]
+
+    # Test for a chunk size of 128 Bytes on TS.ADD
+    assert await modclient.ts().add("time-serie-1", 1, 10.0, chunk_size=128)
+    info = await modclient.ts().info("time-serie-1")
+    assert 128 == info.chunk_size
+
+
+@pytest.mark.redismod
+@skip_ifmodversion_lt("1.4.0", "timeseries")
+async def test_add_duplicate_policy(modclient: redis.Redis):
+
+    # Test for duplicate policy BLOCK
+    assert 1 == await modclient.ts().add("time-serie-add-ooo-block", 1, 5.0)
+    with pytest.raises(Exception):
+        await modclient.ts().add(
+            "time-serie-add-ooo-block", 1, 5.0, duplicate_policy="block"
+        )
+
+    # Test for duplicate policy LAST
+    assert 1 == await modclient.ts().add("time-serie-add-ooo-last", 1, 5.0)
+    assert 1 == await modclient.ts().add(
+        "time-serie-add-ooo-last", 1, 10.0, duplicate_policy="last"
+    )
+    res = await modclient.ts().get("time-serie-add-ooo-last")
+    assert 10.0 == res[1]
+
+    # Test for duplicate policy FIRST
+    assert 1 == await modclient.ts().add("time-serie-add-ooo-first", 1, 5.0)
+    assert 1 == await modclient.ts().add(
+        "time-serie-add-ooo-first", 1, 10.0, duplicate_policy="first"
+    )
+    res = await modclient.ts().get("time-serie-add-ooo-first")
+    assert 5.0 == res[1]
+
+    # Test for duplicate policy MAX
+    assert 1 == await modclient.ts().add("time-serie-add-ooo-max", 1, 5.0)
+    assert 1 == await modclient.ts().add(
+        "time-serie-add-ooo-max", 1, 10.0, duplicate_policy="max"
+    )
+    res = await modclient.ts().get("time-serie-add-ooo-max")
+    assert 10.0 == res[1]
+
+    # Test for duplicate policy MIN
+    assert 1 == await modclient.ts().add("time-serie-add-ooo-min", 1, 5.0)
+    assert 1 == await modclient.ts().add(
+        "time-serie-add-ooo-min", 1, 10.0, duplicate_policy="min"
+    )
+    res = await modclient.ts().get("time-serie-add-ooo-min")
+    assert 5.0 == res[1]
+
+
+@pytest.mark.redismod
+async def test_madd(modclient: redis.Redis):
+    await modclient.ts().create("a")
+    assert [1, 2, 3] == await modclient.ts().madd(
+        [("a", 1, 5), ("a", 2, 10), ("a", 3, 15)]
+    )
+
+
+@pytest.mark.redismod
+async def test_incrby_decrby(modclient: redis.Redis):
+    for _ in range(100):
+        assert await modclient.ts().incrby(1, 1)
+        sleep(0.001)
+    assert 100 == (await modclient.ts().get(1))[1]
+    for _ in range(100):
+        assert await modclient.ts().decrby(1, 1)
+        sleep(0.001)
+    assert 0 == (await modclient.ts().get(1))[1]
+
+    assert await modclient.ts().incrby(2, 1.5, timestamp=5)
+    assert (5, 1.5) == await modclient.ts().get(2)
+    assert await modclient.ts().incrby(2, 2.25, timestamp=7)
+    assert (7, 3.75) == await modclient.ts().get(2)
+    assert await modclient.ts().decrby(2, 1.5, timestamp=15)
+    assert (15, 2.25) == await modclient.ts().get(2)
+
+    # Test for a chunk size of 128 Bytes on TS.INCRBY
+    assert await modclient.ts().incrby("time-serie-1", 10, chunk_size=128)
+    info = await modclient.ts().info("time-serie-1")
+    assert 128 == info.chunk_size
+
+    # Test for a chunk size of 128 Bytes on TS.DECRBY
+    assert await modclient.ts().decrby("time-serie-2", 10, chunk_size=128)
+    info = await modclient.ts().info("time-serie-2")
+    assert 128 == info.chunk_size
+
+
+@pytest.mark.redismod
+async def test_create_and_delete_rule(modclient: redis.Redis):
+    # test rule creation
+    time = 100
+    await modclient.ts().create(1)
+    await modclient.ts().create(2)
+    await modclient.ts().createrule(1, 2, "avg", 100)
+    for i in range(50):
+        await modclient.ts().add(1, time + i * 2, 1)
+        await modclient.ts().add(1, time + i * 2 + 1, 2)
+    await modclient.ts().add(1, time * 2, 1.5)
+    assert round((await modclient.ts().get(2))[1], 5) == 1.5
+    info = await modclient.ts().info(1)
+    assert info.rules[0][1] == 100
+
+    # test rule deletion
+    await modclient.ts().deleterule(1, 2)
+    info = await modclient.ts().info(1)
+    assert not info.rules
+
+
+@pytest.mark.redismod
+@skip_ifmodversion_lt("99.99.99", "timeseries")
+async def test_del_range(modclient: redis.Redis):
+    try:
+        await modclient.ts().delete("test", 0, 100)
+    except Exception as e:
+        assert e.__str__() != ""
+
+    for i in range(100):
+        await modclient.ts().add(1, i, i % 7)
+    assert 22 == await modclient.ts().delete(1, 0, 21)
+    assert [] == await modclient.ts().range(1, 0, 21)
+    assert [(22, 1.0)] == await modclient.ts().range(1, 22, 22)
+
+
+@pytest.mark.redismod
+async def test_range(modclient: redis.Redis):
+    for i in range(100):
+        await modclient.ts().add(1, i, i % 7)
+    assert 100 == len(await modclient.ts().range(1, 0, 200))
+    for i in range(100):
+        await modclient.ts().add(1, i + 200, i % 7)
+    assert 200 == len(await modclient.ts().range(1, 0, 500))
+    # last sample isn't returned
+    assert 20 == len(
+        await modclient.ts().range(
+            1, 0, 500, aggregation_type="avg", bucket_size_msec=10
+        )
+    )
+    assert 10 == len(await modclient.ts().range(1, 0, 500, count=10))
+
+
+@pytest.mark.redismod
+@skip_ifmodversion_lt("99.99.99", "timeseries")
+async def test_range_advanced(modclient: redis.Redis):
+    for i in range(100):
+        await modclient.ts().add(1, i, i % 7)
+        await modclient.ts().add(1, i + 200, i % 7)
+
+    assert 2 == len(
+        await modclient.ts().range(
+            1,
+            0,
+            500,
+            filter_by_ts=[i for i in range(10, 20)],
+            filter_by_min_value=1,
+            filter_by_max_value=2,
+        )
+    )
+    assert [(0, 10.0), (10, 1.0)] == await modclient.ts().range(
+        1, 0, 10, aggregation_type="count", bucket_size_msec=10, align="+"
+    )
+    assert [(0, 5.0), (5, 6.0)] == await modclient.ts().range(
+        1, 0, 10, aggregation_type="count", bucket_size_msec=10, align=5
+    )
+
+
+@pytest.mark.redismod
+@skip_ifmodversion_lt("99.99.99", "timeseries")
+async def test_rev_range(modclient: redis.Redis):
+    for i in range(100):
+        await modclient.ts().add(1, i, i % 7)
+    assert 100 == len(await modclient.ts().range(1, 0, 200))
+    for i in range(100):
+        await modclient.ts().add(1, i + 200, i % 7)
+    assert 200 == len(await modclient.ts().range(1, 0, 500))
+    # first sample isn't returned
+    assert 20 == len(
+        await modclient.ts().revrange(
+            1, 0, 500, aggregation_type="avg", bucket_size_msec=10
+        )
+    )
+    assert 10 == len(await modclient.ts().revrange(1, 0, 500, count=10))
+    assert 2 == len(
+        await modclient.ts().revrange(
+            1,
+            0,
+            500,
+            filter_by_ts=[i for i in range(10, 20)],
+            filter_by_min_value=1,
+            filter_by_max_value=2,
+        )
+    )
+    assert [(10, 1.0), (0, 10.0)] == await modclient.ts().revrange(
+        1, 0, 10, aggregation_type="count", bucket_size_msec=10, align="+"
+    )
+    assert [(1, 10.0), (0, 1.0)] == await modclient.ts().revrange(
+        1, 0, 10, aggregation_type="count", bucket_size_msec=10, align=1
+    )
+
+
+@pytest.mark.redismod
+@pytest.mark.onlynoncluster
+async def testMultiRange(modclient: redis.Redis):
+    await modclient.ts().create(1, labels={"Test": "This", "team": "ny"})
+    await modclient.ts().create(
+        2, labels={"Test": "This", "Taste": "That", "team": "sf"}
+    )
+    for i in range(100):
+        await modclient.ts().add(1, i, i % 7)
+        await modclient.ts().add(2, i, i % 11)
+
+    res = await modclient.ts().mrange(0, 200, filters=["Test=This"])
+    assert 2 == len(res)
+    assert 100 == len(res[0]["1"][1])
+
+    res = await modclient.ts().mrange(0, 200, filters=["Test=This"], count=10)
+    assert 10 == len(res[0]["1"][1])
+
+    for i in range(100):
+        await modclient.ts().add(1, i + 200, i % 7)
+    res = await modclient.ts().mrange(
+        0, 500, filters=["Test=This"], aggregation_type="avg", bucket_size_msec=10
+    )
+    assert 2 == len(res)
+    assert 20 == len(res[0]["1"][1])
+
+    # test withlabels
+    assert {} == res[0]["1"][0]
+    res = await modclient.ts().mrange(0, 200, filters=["Test=This"], with_labels=True)
+    assert {"Test": "This", "team": "ny"} == res[0]["1"][0]
+
+
+@pytest.mark.redismod
+@pytest.mark.onlynoncluster
+@skip_ifmodversion_lt("99.99.99", "timeseries")
+async def test_multi_range_advanced(modclient: redis.Redis):
+    await modclient.ts().create(1, labels={"Test": "This", "team": "ny"})
+    await modclient.ts().create(
+        2, labels={"Test": "This", "Taste": "That", "team": "sf"}
+    )
+    for i in range(100):
+        await modclient.ts().add(1, i, i % 7)
+        await modclient.ts().add(2, i, i % 11)
+
+    # test with selected labels
+    res = await modclient.ts().mrange(
+        0, 200, filters=["Test=This"], select_labels=["team"]
+    )
+    assert {"team": "ny"} == res[0]["1"][0]
+    assert {"team": "sf"} == res[1]["2"][0]
+
+    # test with filterby
+    res = await modclient.ts().mrange(
+        0,
+        200,
+        filters=["Test=This"],
+        filter_by_ts=[i for i in range(10, 20)],
+        filter_by_min_value=1,
+        filter_by_max_value=2,
+    )
+    assert [(15, 1.0), (16, 2.0)] == res[0]["1"][1]
+
+    # test groupby
+    res = await modclient.ts().mrange(
+        0, 3, filters=["Test=This"], groupby="Test", reduce="sum"
+    )
+    assert [(0, 0.0), (1, 2.0), (2, 4.0), (3, 6.0)] == res[0]["Test=This"][1]
+    res = await modclient.ts().mrange(
+        0, 3, filters=["Test=This"], groupby="Test", reduce="max"
+    )
+    assert [(0, 0.0), (1, 1.0), (2, 2.0), (3, 3.0)] == res[0]["Test=This"][1]
+    res = await modclient.ts().mrange(
+        0, 3, filters=["Test=This"], groupby="team", reduce="min"
+    )
+    assert 2 == len(res)
+    assert [(0, 0.0), (1, 1.0), (2, 2.0), (3, 3.0)] == res[0]["team=ny"][1]
+    assert [(0, 0.0), (1, 1.0), (2, 2.0), (3, 3.0)] == res[1]["team=sf"][1]
+
+    # test align
+    res = await modclient.ts().mrange(
+        0,
+        10,
+        filters=["team=ny"],
+        aggregation_type="count",
+        bucket_size_msec=10,
+        align="-",
+    )
+    assert [(0, 10.0), (10, 1.0)] == res[0]["1"][1]
+    res = await modclient.ts().mrange(
+        0,
+        10,
+        filters=["team=ny"],
+        aggregation_type="count",
+        bucket_size_msec=10,
+        align=5,
+    )
+    assert [(0, 5.0), (5, 6.0)] == res[0]["1"][1]
+
+
+@pytest.mark.redismod
+@pytest.mark.onlynoncluster
+@skip_ifmodversion_lt("99.99.99", "timeseries")
+async def test_multi_reverse_range(modclient: redis.Redis):
+    await modclient.ts().create(1, labels={"Test": "This", "team": "ny"})
+    await modclient.ts().create(
+        2, labels={"Test": "This", "Taste": "That", "team": "sf"}
+    )
+    for i in range(100):
+        await modclient.ts().add(1, i, i % 7)
+        await modclient.ts().add(2, i, i % 11)
+
+    res = await modclient.ts().mrange(0, 200, filters=["Test=This"])
+    assert 2 == len(res)
+    assert 100 == len(res[0]["1"][1])
+
+    res = await modclient.ts().mrange(0, 200, filters=["Test=This"], count=10)
+    assert 10 == len(res[0]["1"][1])
+
+    for i in range(100):
+        await modclient.ts().add(1, i + 200, i % 7)
+    res = await modclient.ts().mrevrange(
+        0, 500, filters=["Test=This"], aggregation_type="avg", bucket_size_msec=10
+    )
+    assert 2 == len(res)
+    assert 20 == len(res[0]["1"][1])
+    assert {} == res[0]["1"][0]
+
+    # test withlabels
+    res = await modclient.ts().mrevrange(
+        0, 200, filters=["Test=This"], with_labels=True
+    )
+    assert {"Test": "This", "team": "ny"} == res[0]["1"][0]
+
+    # test with selected labels
+    res = await modclient.ts().mrevrange(
+        0, 200, filters=["Test=This"], select_labels=["team"]
+    )
+    assert {"team": "ny"} == res[0]["1"][0]
+    assert {"team": "sf"} == res[1]["2"][0]
+
+    # test filterby
+    res = await modclient.ts().mrevrange(
+        0,
+        200,
+        filters=["Test=This"],
+        filter_by_ts=[i for i in range(10, 20)],
+        filter_by_min_value=1,
+        filter_by_max_value=2,
+    )
+    assert [(16, 2.0), (15, 1.0)] == res[0]["1"][1]
+
+    # test groupby
+    res = await modclient.ts().mrevrange(
+        0, 3, filters=["Test=This"], groupby="Test", reduce="sum"
+    )
+    assert [(3, 6.0), (2, 4.0), (1, 2.0), (0, 0.0)] == res[0]["Test=This"][1]
+    res = await modclient.ts().mrevrange(
+        0, 3, filters=["Test=This"], groupby="Test", reduce="max"
+    )
+    assert [(3, 3.0), (2, 2.0), (1, 1.0), (0, 0.0)] == res[0]["Test=This"][1]
+    res = await modclient.ts().mrevrange(
+        0, 3, filters=["Test=This"], groupby="team", reduce="min"
+    )
+    assert 2 == len(res)
+    assert [(3, 3.0), (2, 2.0), (1, 1.0), (0, 0.0)] == res[0]["team=ny"][1]
+    assert [(3, 3.0), (2, 2.0), (1, 1.0), (0, 0.0)] == res[1]["team=sf"][1]
+
+    # test align
+    res = await modclient.ts().mrevrange(
+        0,
+        10,
+        filters=["team=ny"],
+        aggregation_type="count",
+        bucket_size_msec=10,
+        align="-",
+    )
+    assert [(10, 1.0), (0, 10.0)] == res[0]["1"][1]
+    res = await modclient.ts().mrevrange(
+        0,
+        10,
+        filters=["team=ny"],
+        aggregation_type="count",
+        bucket_size_msec=10,
+        align=1,
+    )
+    assert [(1, 10.0), (0, 1.0)] == res[0]["1"][1]
+
+
+@pytest.mark.redismod
+async def test_get(modclient: redis.Redis):
+    name = "test"
+    await modclient.ts().create(name)
+    assert await modclient.ts().get(name) is None
+    await modclient.ts().add(name, 2, 3)
+    assert 2 == (await modclient.ts().get(name))[0]
+    await modclient.ts().add(name, 3, 4)
+    assert 4 == (await modclient.ts().get(name))[1]
+
+
+@pytest.mark.redismod
+@pytest.mark.onlynoncluster
+async def test_mget(modclient: redis.Redis):
+    await modclient.ts().create(1, labels={"Test": "This"})
+    await modclient.ts().create(2, labels={"Test": "This", "Taste": "That"})
+    act_res = await modclient.ts().mget(["Test=This"])
+    exp_res = [{"1": [{}, None, None]}, {"2": [{}, None, None]}]
+    assert act_res == exp_res
+    await modclient.ts().add(1, "*", 15)
+    await modclient.ts().add(2, "*", 25)
+    res = await modclient.ts().mget(["Test=This"])
+    assert 15 == res[0]["1"][2]
+    assert 25 == res[1]["2"][2]
+    res = await modclient.ts().mget(["Taste=That"])
+    assert 25 == res[0]["2"][2]
+
+    # test with_labels
+    assert {} == res[0]["2"][0]
+    res = await modclient.ts().mget(["Taste=That"], with_labels=True)
+    assert {"Taste": "That", "Test": "This"} == res[0]["2"][0]
+
+
+@pytest.mark.redismod
+async def test_info(modclient: redis.Redis):
+    await modclient.ts().create(
+        1, retention_msecs=5, labels={"currentLabel": "currentData"}
+    )
+    info = await modclient.ts().info(1)
+    assert 5 == info.retention_msecs
+    assert info.labels["currentLabel"] == "currentData"
+
+
+@pytest.mark.redismod
+@skip_ifmodversion_lt("1.4.0", "timeseries")
+async def testInfoDuplicatePolicy(modclient: redis.Redis):
+    await modclient.ts().create(
+        1, retention_msecs=5, labels={"currentLabel": "currentData"}
+    )
+    info = await modclient.ts().info(1)
+    assert info.duplicate_policy is None
+
+    await modclient.ts().create("time-serie-2", duplicate_policy="min")
+    info = await modclient.ts().info("time-serie-2")
+    assert "min" == info.duplicate_policy
+
+
+@pytest.mark.redismod
+@pytest.mark.onlynoncluster
+async def test_query_index(modclient: redis.Redis):
+    await modclient.ts().create(1, labels={"Test": "This"})
+    await modclient.ts().create(2, labels={"Test": "This", "Taste": "That"})
+    assert 2 == len(await modclient.ts().queryindex(["Test=This"]))
+    assert 1 == len(await modclient.ts().queryindex(["Taste=That"]))
+    assert [2] == await modclient.ts().queryindex(["Taste=That"])
+
+
+# @pytest.mark.redismod
+# async def test_pipeline(modclient: redis.Redis):
+#     pipeline = await modclient.ts().pipeline()
+#     pipeline.create("with_pipeline")
+#     for i in range(100):
+#         pipeline.add("with_pipeline", i, 1.1 * i)
+#     pipeline.execute()
+
+#     info = await modclient.ts().info("with_pipeline")
+#     assert info.lastTimeStamp == 99
+#     assert info.total_samples == 100
+#     assert await modclient.ts().get("with_pipeline")[1] == 99 * 1.1
+
+
+@pytest.mark.redismod
+async def test_uncompressed(modclient: redis.Redis):
+    await modclient.ts().create("compressed")
+    await modclient.ts().create("uncompressed", uncompressed=True)
+    compressed_info = await modclient.ts().info("compressed")
+    uncompressed_info = await modclient.ts().info("uncompressed")
+    assert compressed_info.memory_usage != uncompressed_info.memory_usage