From 63d50ed607a8fd925e157857337aff064c12b799 Mon Sep 17 00:00:00 2001 From: dvora-h Date: Thu, 24 Feb 2022 15:05:37 +0200 Subject: [PATCH 1/6] command docs --- redis/commands/core.py | 9 +++++++++ tests/test_commands.py | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/redis/commands/core.py b/redis/commands/core.py index 7fd668e6ae..7f1fb0baee 100644 --- a/redis/commands/core.py +++ b/redis/commands/core.py @@ -741,6 +741,15 @@ def command_info(self, **kwargs) -> None: def command_count(self, **kwargs) -> ResponseT: return self.execute_command("COMMAND COUNT", **kwargs) + def command_docs(self, *args): + """ + This function throws a NotImplementedError since it is intentionally + not supported. + """ + raise NotImplementedError( + "COMMAND DOCS is intentionally not implemented in the client." + ) + def config_get(self, pattern: PatternT = "*", **kwargs) -> ResponseT: """ Return a dictionary of configuration based on the ``pattern`` diff --git a/tests/test_commands.py b/tests/test_commands.py index 5c32d5f296..de37debd62 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -4212,6 +4212,11 @@ def test_command_count(self, r): assert isinstance(res, int) assert res >= 100 + # @skip_if_server_versiov_lt("7.0.0") + def test_command_docs(self, unstable_r): + with pytest.raises(NotImplementedError): + unstable_r.command_docs("set") + @pytest.mark.onlynoncluster @skip_if_server_version_lt("2.8.13") def test_command_getkeys(self, r): From a10e0c6575ebefdf96ddb7d6b6ece700958f73c3 Mon Sep 17 00:00:00 2001 From: dogukanteber <47397379+dogukanteber@users.noreply.github.com> Date: Wed, 2 Mar 2022 13:27:12 +0300 Subject: [PATCH 2/6] Add support for AUTH (#1929) * Add support for AUTH * Fix linter error * test fix * fix test in cluster Co-authored-by: Chayim Co-authored-by: Chayim I. Kirshen Co-authored-by: dvora-h --- redis/cluster.py | 1 + redis/commands/core.py | 14 ++++++++------ tests/test_commands.py | 24 ++++++++++++++++++++---- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/redis/cluster.py b/redis/cluster.py index 3b30a6e0a8..4491e29f5a 100644 --- a/redis/cluster.py +++ b/redis/cluster.py @@ -228,6 +228,7 @@ class RedisCluster(RedisClusterCommands): "ACL SETUSER", "ACL USERS", "ACL WHOAMI", + "AUTH", "CLIENT LIST", "CLIENT SETNAME", "CLIENT GETNAME", diff --git a/redis/commands/core.py b/redis/commands/core.py index 73fc719100..d5ca75b644 100644 --- a/redis/commands/core.py +++ b/redis/commands/core.py @@ -369,14 +369,16 @@ class ManagementCommands(CommandsProtocol): Redis management commands """ - def auth(self): + def auth(self, password, username=None, **kwargs): """ - This function throws a NotImplementedError since it is intentionally - not supported. + Authenticates the user. If you do not pass username, Redis will try to + authenticate for the "default" user. If you do pass username, it will + authenticate for the given user. + For more information check https://redis.io/commands/auth """ - raise NotImplementedError( - "AUTH is intentionally not implemented in the client." - ) + if username: + return self.execute_command("AUTH", username, password, **kwargs) + return self.execute_command def bgrewriteaof(self, **kwargs): """Tell the Redis server to rewrite the AOF file from data in memory. diff --git a/tests/test_commands.py b/tests/test_commands.py index de37debd62..0308b6ca0d 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -65,16 +65,32 @@ def test_case_insensitive_command_names(self, r): class TestRedisCommands: + def test_auth(self, r, request): + username = "redis-py-auth" + + def teardown(): + r.acl_deluser(username) + + request.addfinalizer(teardown) + + assert r.acl_setuser( + username, + enabled=True, + passwords=["+strong_password"], + commands=["+acl"], + ) + + assert r.auth(username=username, password="strong_password") is True + + with pytest.raises(exceptions.ResponseError): + r.auth(username=username, password="wrong_password") + def test_command_on_invalid_key_type(self, r): r.lpush("a", "1") with pytest.raises(redis.ResponseError): r["a"] # SERVER INFORMATION - def test_auth_not_implemented(self, r): - with pytest.raises(NotImplementedError): - r.auth() - @skip_if_server_version_lt("6.0.0") def test_acl_cat_no_category(self, r): categories = r.acl_cat() From 25f7992d93c448a3ff92bd2339b01c3c80af03df Mon Sep 17 00:00:00 2001 From: dvora-h <67596500+dvora-h@users.noreply.github.com> Date: Sun, 6 Mar 2022 13:49:13 +0200 Subject: [PATCH 3/6] Add support for JSON, TIMESERIES, BLOOM & GRAPH commands in cluster (#2032) Co-authored-by: Chayim --- redis/cluster.py | 30 +++++++++++++++++++++++++- redis/commands/cluster.py | 2 ++ redis/commands/json/__init__.py | 30 ++++++++++++++++++++------ redis/commands/parser.py | 9 +++++++- redis/commands/timeseries/__init__.py | 31 +++++++++++++++++++++------ tests/test_bloom.py | 1 + tests/test_graph.py | 1 + tests/test_search.py | 3 +++ tests/test_timeseries.py | 5 +++++ tox.ini | 2 +- 10 files changed, 98 insertions(+), 16 deletions(-) diff --git a/redis/cluster.py b/redis/cluster.py index 4491e29f5a..4b327ad79e 100644 --- a/redis/cluster.py +++ b/redis/cluster.py @@ -284,6 +284,7 @@ class RedisCluster(RedisClusterCommands): "READONLY", "READWRITE", "TIME", + "GRAPH.CONFIG", ], DEFAULT_NODE, ), @@ -810,6 +811,10 @@ def lock( thread_local=thread_local, ) + def set_response_callback(self, command, callback): + """Set a custom Response Callback""" + self.cluster_response_callbacks[command] = callback + def _determine_nodes(self, *args, **kwargs): command = args[0] nodes_flag = kwargs.pop("nodes_flag", None) @@ -1181,6 +1186,20 @@ def _process_result(self, command, res, **kwargs): else: return res + def load_external_module( + self, + funcname, + func, + ): + """ + This function can be used to add externally defined redis modules, + and their namespaces to the redis client. + + ``funcname`` - A string containing the name of the function to create + ``func`` - The function, being added to this class. + """ + setattr(self, funcname, func) + class ClusterNode: def __init__(self, host, port, server_type=None, redis_connection=None): @@ -2026,7 +2045,13 @@ def _send_cluster_commands( # turn the response back into a simple flat array that corresponds # to the sequence of commands issued in the stack in pipeline.execute() - response = [c.result for c in sorted(stack, key=lambda x: x.position)] + response = [] + for c in sorted(stack, key=lambda x: x.position): + if c.args[0] in self.cluster_response_callbacks: + c.result = self.cluster_response_callbacks[c.args[0]]( + c.result, **c.options + ) + response.append(c.result) if raise_on_error: self.raise_first_error(stack) @@ -2040,6 +2065,9 @@ def _fail_on_redirect(self, allow_redirections): "ASK & MOVED redirection not allowed in this pipeline" ) + def exists(self, *keys): + return self.execute_command("EXISTS", *keys) + def eval(self): """ """ raise RedisClusterException("method eval() is not implemented") diff --git a/redis/commands/cluster.py b/redis/commands/cluster.py index 8bdcbbadf6..7342c0c482 100644 --- a/redis/commands/cluster.py +++ b/redis/commands/cluster.py @@ -9,6 +9,7 @@ ScriptCommands, ) from .helpers import list_or_args +from .redismodules import RedisModuleCommands class ClusterMultiKeyCommands: @@ -212,6 +213,7 @@ class RedisClusterCommands( PubSubCommands, ClusterDataAccessCommands, ScriptCommands, + RedisModuleCommands, ): """ A class for all Redis Cluster commands diff --git a/redis/commands/json/__init__.py b/redis/commands/json/__init__.py index 12c0648722..638e4eb166 100644 --- a/redis/commands/json/__init__.py +++ b/redis/commands/json/__init__.py @@ -103,16 +103,34 @@ def pipeline(self, transaction=True, shard_hint=None): pipe.jsonget('foo') pipe.jsonget('notakey') """ - p = Pipeline( - connection_pool=self.client.connection_pool, - response_callbacks=self.MODULE_CALLBACKS, - transaction=transaction, - shard_hint=shard_hint, - ) + if isinstance(self.client, redis.RedisCluster): + p = ClusterPipeline( + nodes_manager=self.client.nodes_manager, + commands_parser=self.client.commands_parser, + startup_nodes=self.client.nodes_manager.startup_nodes, + result_callbacks=self.client.result_callbacks, + cluster_response_callbacks=self.client.cluster_response_callbacks, + cluster_error_retry_attempts=self.client.cluster_error_retry_attempts, + read_from_replicas=self.client.read_from_replicas, + reinitialize_steps=self.client.reinitialize_steps, + ) + + else: + p = Pipeline( + connection_pool=self.client.connection_pool, + response_callbacks=self.MODULE_CALLBACKS, + transaction=transaction, + shard_hint=shard_hint, + ) + p._encode = self._encode p._decode = self._decode return p +class ClusterPipeline(JSONCommands, redis.cluster.ClusterPipeline): + """Cluster pipeline for the module.""" + + class Pipeline(JSONCommands, redis.client.Pipeline): """Pipeline for the module.""" diff --git a/redis/commands/parser.py b/redis/commands/parser.py index 2bb0576910..89292ab2d3 100644 --- a/redis/commands/parser.py +++ b/redis/commands/parser.py @@ -17,7 +17,14 @@ def __init__(self, redis_connection): self.initialize(redis_connection) def initialize(self, r): - self.commands = r.execute_command("COMMAND") + commands = r.execute_command("COMMAND") + uppercase_commands = [] + for cmd in commands: + if any(x.isupper() for x in cmd): + uppercase_commands.append(cmd) + for cmd in uppercase_commands: + commands[cmd.lower()] = commands.pop(cmd) + self.commands = commands # As soon as this PR is merged into Redis, we should reimplement # our logic to use COMMAND INFO changes to determine the key positions diff --git a/redis/commands/timeseries/__init__.py b/redis/commands/timeseries/__init__.py index 5b1f15114d..4720a430f8 100644 --- a/redis/commands/timeseries/__init__.py +++ b/redis/commands/timeseries/__init__.py @@ -1,4 +1,4 @@ -import redis.client +import redis from ..helpers import parse_to_list from .commands import ( @@ -67,14 +67,31 @@ def pipeline(self, transaction=True, shard_hint=None): pipeline.execute() """ - p = Pipeline( - connection_pool=self.client.connection_pool, - response_callbacks=self.MODULE_CALLBACKS, - transaction=transaction, - shard_hint=shard_hint, - ) + if isinstance(self.client, redis.RedisCluster): + p = ClusterPipeline( + nodes_manager=self.client.nodes_manager, + commands_parser=self.client.commands_parser, + startup_nodes=self.client.nodes_manager.startup_nodes, + result_callbacks=self.client.result_callbacks, + cluster_response_callbacks=self.client.cluster_response_callbacks, + cluster_error_retry_attempts=self.client.cluster_error_retry_attempts, + read_from_replicas=self.client.read_from_replicas, + reinitialize_steps=self.client.reinitialize_steps, + ) + + else: + p = Pipeline( + connection_pool=self.client.connection_pool, + response_callbacks=self.MODULE_CALLBACKS, + transaction=transaction, + shard_hint=shard_hint, + ) return p +class ClusterPipeline(TimeSeriesCommands, redis.cluster.ClusterPipeline): + """Cluster pipeline for the module.""" + + class Pipeline(TimeSeriesCommands, redis.client.Pipeline): """Pipeline for the module.""" diff --git a/tests/test_bloom.py b/tests/test_bloom.py index 8936584ea8..a3e9e158f4 100644 --- a/tests/test_bloom.py +++ b/tests/test_bloom.py @@ -191,6 +191,7 @@ def test_cms(client): @pytest.mark.redismod +@pytest.mark.onlynoncluster def test_cms_merge(client): assert client.cms().initbydim("A", 1000, 5) assert client.cms().initbydim("B", 1000, 5) diff --git a/tests/test_graph.py b/tests/test_graph.py index 8de63465a0..3a430ed4ab 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -342,6 +342,7 @@ def test_config(client): @pytest.mark.redismod +@pytest.mark.onlynoncluster def test_list_keys(client): result = client.graph().list_keys() assert result == [] diff --git a/tests/test_search.py b/tests/test_search.py index 5ee17a2c36..88d57a921d 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -21,6 +21,9 @@ from .conftest import default_redismod_url, skip_ifmodversion_lt +pytestmark = pytest.mark.onlynoncluster + + WILL_PLAY_TEXT = os.path.abspath( os.path.join(os.path.dirname(__file__), "testdata", "will_play_text.csv.bz2") ) diff --git a/tests/test_timeseries.py b/tests/test_timeseries.py index aee37aaa43..421c9d5c04 100644 --- a/tests/test_timeseries.py +++ b/tests/test_timeseries.py @@ -264,6 +264,7 @@ def test_rev_range(client): @pytest.mark.redismod +@pytest.mark.onlynoncluster def testMultiRange(client): client.ts().create(1, labels={"Test": "This", "team": "ny"}) client.ts().create(2, labels={"Test": "This", "Taste": "That", "team": "sf"}) @@ -293,6 +294,7 @@ def testMultiRange(client): @pytest.mark.redismod +@pytest.mark.onlynoncluster @skip_ifmodversion_lt("99.99.99", "timeseries") def test_multi_range_advanced(client): client.ts().create(1, labels={"Test": "This", "team": "ny"}) @@ -349,6 +351,7 @@ def test_multi_range_advanced(client): @pytest.mark.redismod +@pytest.mark.onlynoncluster @skip_ifmodversion_lt("99.99.99", "timeseries") def test_multi_reverse_range(client): client.ts().create(1, labels={"Test": "This", "team": "ny"}) @@ -442,6 +445,7 @@ def test_get(client): @pytest.mark.redismod +@pytest.mark.onlynoncluster def test_mget(client): client.ts().create(1, labels={"Test": "This"}) client.ts().create(2, labels={"Test": "This", "Taste": "That"}) @@ -483,6 +487,7 @@ def testInfoDuplicatePolicy(client): @pytest.mark.redismod +@pytest.mark.onlynoncluster def test_query_index(client): client.ts().create(1, labels={"Test": "This"}) client.ts().create(2, labels={"Test": "This", "Taste": "That"}) diff --git a/tox.ini b/tox.ini index 82e79d7611..d4f15acd50 100644 --- a/tox.ini +++ b/tox.ini @@ -286,7 +286,7 @@ setenv = commands = standalone: pytest --cov=./ --cov-report=xml:coverage_redis.xml -W always -m 'not onlycluster' {posargs} standalone-uvloop: pytest --cov=./ --cov-report=xml:coverage_redis.xml -W always -m 'not onlycluster' --uvloop {posargs} - cluster: pytest --cov=./ --cov-report=xml:coverage_cluster.xml -W always -m 'not onlynoncluster and not redismod' --redis-url={env:CLUSTER_URL:} {posargs} + cluster: pytest --cov=./ --cov-report=xml:coverage_cluster.xml -W always -m 'not onlynoncluster and not redismod' --redis-url={env:CLUSTER_URL:} --redismod-url={env:CLUSTER_URL:} {posargs} cluster-uvloop: pytest --cov=./ --cov-report=xml:coverage_redis.xml -W always -m 'not onlycluster' --uvloop {posargs} [testenv:redis5] From 67e2c64e0d6012b0817d869b5c23c73e0f874702 Mon Sep 17 00:00:00 2001 From: Marek Czaplicki Date: Sun, 6 Mar 2022 13:29:01 +0100 Subject: [PATCH 4/6] Add support for HSET items (#2006) * Add `items` parameter to `hset` * Add test for `hset` with `items` * Update CHANGES * fix test_profile Co-authored-by: Chayim Co-authored-by: dvora-h --- CHANGES | 3 +++ redis/commands/core.py | 7 +++++-- tests/test_commands.py | 11 +++++++++++ tests/test_search.py | 2 +- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index ddf7cfd4e3..ca2d2e818d 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,9 @@ + + * Add `items` parameter to `hset` signature * Create codeql-analysis.yml (#1988). Thanks @chayim * Add limited support for Lua scripting with RedisCluster * Implement `.lock()` method on RedisCluster + * 4.1.3 (Feb 8, 2022) * Fix flushdb and flushall (#1926) * Add redis5 and redis4 dockers (#1871) diff --git a/redis/commands/core.py b/redis/commands/core.py index d5ca75b644..e9bb286593 100644 --- a/redis/commands/core.py +++ b/redis/commands/core.py @@ -4598,18 +4598,21 @@ def hset( key: Optional[str] = None, value: Optional[str] = None, mapping: Optional[dict] = None, + items: Optional[list] = None, ) -> Union[Awaitable[int], int]: """ Set ``key`` to ``value`` within hash ``name``, ``mapping`` accepts a dict of key/value pairs that will be added to hash ``name``. + ``items`` accepts a list of key/value pairs that will be + added to hash ``name``. Returns the number of fields that were added. For more information check https://redis.io/commands/hset """ - if key is None and not mapping: + if key is None and not mapping and not items: raise DataError("'hset' with no key value pairs") - items = [] + items = items or [] if key is not None: items.extend((key, value)) if mapping: diff --git a/tests/test_commands.py b/tests/test_commands.py index 0308b6ca0d..c9b04669ea 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -2546,6 +2546,17 @@ def test_hset_with_multi_key_values(self, r): assert r.hget("b", "2") == b"2" assert r.hget("b", "foo") == b"bar" + def test_hset_with_key_values_passed_as_list(self, r): + r.hset("a", items=["1", 1, "2", 2, "3", 3]) + assert r.hget("a", "1") == b"1" + assert r.hget("a", "2") == b"2" + assert r.hget("a", "3") == b"3" + + r.hset("b", "foo", "bar", items=["1", 1, "2", 2]) + assert r.hget("b", "1") == b"1" + assert r.hget("b", "2") == b"2" + assert r.hget("b", "foo") == b"bar" + def test_hset_without_data(self, r): with pytest.raises(exceptions.DataError): r.hset("x") diff --git a/tests/test_search.py b/tests/test_search.py index 88d57a921d..5eb8e9762a 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -1500,7 +1500,7 @@ def test_profile(client): res, det = client.ft().profile(req) assert det["Iterators profile"]["Counter"] == 2.0 assert det["Iterators profile"]["Type"] == "WILDCARD" - assert det["Parsing time"] < 0.5 + assert isinstance(det["Parsing time"], float) assert len(res.rows) == 2 # check also the search result From f789b48c76b00510496075f4a11bfbd84531dbc8 Mon Sep 17 00:00:00 2001 From: dvora-h <67596500+dvora-h@users.noreply.github.com> Date: Sun, 6 Mar 2022 21:25:34 +0200 Subject: [PATCH 5/6] Add cluster support for functions (#2016) * cluster support for functions * fix test_list_on_cluster mark * fix mark * cluster unstable url * fix * fix cluster url * skip tests * linters * linters --- redis/cluster.py | 13 ++++ redis/commands/cluster.py | 2 + tests/test_function.py | 128 +++++++++++++++++++++++--------------- tox.ini | 3 +- 4 files changed, 94 insertions(+), 52 deletions(-) diff --git a/redis/cluster.py b/redis/cluster.py index 4b327ad79e..8c2dfc271b 100644 --- a/redis/cluster.py +++ b/redis/cluster.py @@ -292,12 +292,21 @@ class RedisCluster(RedisClusterCommands): [ "FLUSHALL", "FLUSHDB", + "FUNCTION DELETE", + "FUNCTION FLUSH", + "FUNCTION LIST", + "FUNCTION LOAD", + "FUNCTION RESTORE", "SCRIPT EXISTS", "SCRIPT FLUSH", "SCRIPT LOAD", ], PRIMARIES, ), + list_keys_to_dict( + ["FUNCTION DUMP"], + RANDOM, + ), list_keys_to_dict( [ "CLUSTER COUNTKEYSINSLOT", @@ -916,6 +925,10 @@ def determine_slot(self, *args): else: keys = self._get_command_keys(*args) if keys is None or len(keys) == 0: + # FCALL can call a function with 0 keys, that means the function + # can be run on any node so we can just return a random slot + if command in ("FCALL", "FCALL_RO"): + return random.randrange(0, REDIS_CLUSTER_HASH_SLOTS) raise RedisClusterException( "No way to dispatch this command to Redis Cluster. " "Missing key.\nYou can execute the command by specifying " diff --git a/redis/commands/cluster.py b/redis/commands/cluster.py index 7342c0c482..e14b6e35ee 100644 --- a/redis/commands/cluster.py +++ b/redis/commands/cluster.py @@ -4,6 +4,7 @@ from .core import ( ACLCommands, DataAccessCommands, + FunctionCommands, ManagementCommands, PubSubCommands, ScriptCommands, @@ -213,6 +214,7 @@ class RedisClusterCommands( PubSubCommands, ClusterDataAccessCommands, ScriptCommands, + FunctionCommands, RedisModuleCommands, ): """ diff --git a/tests/test_function.py b/tests/test_function.py index 921ba30444..6f0a6ec1e1 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -2,6 +2,8 @@ from redis.exceptions import ResponseError +from .conftest import skip_if_server_version_lt + function = "redis.register_function('myfunc', function(keys, args) return args[1] end)" function2 = "redis.register_function('hello', function() return 'Hello World' end)" set_function = "redis.register_function('set', function(keys, args) \ @@ -10,42 +12,42 @@ return redis.call('GET', keys[1]) end)" -@pytest.mark.onlynoncluster -# @skip_if_server_version_lt("7.0.0") turn on after redis 7 release +@skip_if_server_version_lt("7.0.0") class TestFunction: @pytest.fixture(autouse=True) - def reset_functions(self, unstable_r): - unstable_r.function_flush() + def reset_functions(self, r): + r.function_flush() - def test_function_load(self, unstable_r): - assert unstable_r.function_load("Lua", "mylib", function) - assert unstable_r.function_load("Lua", "mylib", function, replace=True) + def test_function_load(self, r): + assert r.function_load("Lua", "mylib", function) + assert r.function_load("Lua", "mylib", function, replace=True) with pytest.raises(ResponseError): - unstable_r.function_load("Lua", "mylib", function) + r.function_load("Lua", "mylib", function) with pytest.raises(ResponseError): - unstable_r.function_load("Lua", "mylib2", function) + r.function_load("Lua", "mylib2", function) - def test_function_delete(self, unstable_r): - unstable_r.function_load("Lua", "mylib", set_function) + def test_function_delete(self, r): + r.function_load("Lua", "mylib", set_function) with pytest.raises(ResponseError): - unstable_r.function_load("Lua", "mylib", set_function) - assert unstable_r.fcall("set", 1, "foo", "bar") == "OK" - assert unstable_r.function_delete("mylib") + r.function_load("Lua", "mylib", set_function) + assert r.fcall("set", 1, "foo", "bar") == "OK" + assert r.function_delete("mylib") with pytest.raises(ResponseError): - unstable_r.fcall("set", 1, "foo", "bar") - assert unstable_r.function_load("Lua", "mylib", set_function) + r.fcall("set", 1, "foo", "bar") + assert r.function_load("Lua", "mylib", set_function) - def test_function_flush(self, unstable_r): - unstable_r.function_load("Lua", "mylib", function) - assert unstable_r.fcall("myfunc", 0, "hello") == "hello" - assert unstable_r.function_flush() + def test_function_flush(self, r): + r.function_load("Lua", "mylib", function) + assert r.fcall("myfunc", 0, "hello") == "hello" + assert r.function_flush() with pytest.raises(ResponseError): - unstable_r.fcall("myfunc", 0, "hello") + r.fcall("myfunc", 0, "hello") with pytest.raises(ResponseError): - unstable_r.function_flush("ABC") + r.function_flush("ABC") - def test_function_list(self, unstable_r): - unstable_r.function_load("Lua", "mylib", function) + @pytest.mark.onlynoncluster + def test_function_list(self, r): + r.function_load("Lua", "mylib", function) res = [ [ "library_name", @@ -58,37 +60,61 @@ def test_function_list(self, unstable_r): [["name", "myfunc", "description", None]], ], ] - assert unstable_r.function_list() == res - assert unstable_r.function_list(library="*lib") == res - assert unstable_r.function_list(withcode=True)[0][9] == function + assert r.function_list() == res + assert r.function_list(library="*lib") == res + assert r.function_list(withcode=True)[0][9] == function + + @pytest.mark.onlycluster + def test_function_list_on_cluster(self, r): + r.function_load("Lua", "mylib", function) + function_list = [ + [ + "library_name", + "mylib", + "engine", + "LUA", + "description", + None, + "functions", + [["name", "myfunc", "description", None]], + ], + ] + primaries = r.get_primaries() + res = {} + for node in primaries: + res[node.name] = function_list + assert r.function_list() == res + assert r.function_list(library="*lib") == res + node = primaries[0].name + assert r.function_list(withcode=True)[node][0][9] == function - def test_fcall(self, unstable_r): - unstable_r.function_load("Lua", "mylib", set_function) - unstable_r.function_load("Lua", "mylib2", get_function) - assert unstable_r.fcall("set", 1, "foo", "bar") == "OK" - assert unstable_r.fcall("get", 1, "foo") == "bar" + def test_fcall(self, r): + r.function_load("Lua", "mylib", set_function) + r.function_load("Lua", "mylib2", get_function) + assert r.fcall("set", 1, "foo", "bar") == "OK" + assert r.fcall("get", 1, "foo") == "bar" with pytest.raises(ResponseError): - unstable_r.fcall("myfunc", 0, "hello") + r.fcall("myfunc", 0, "hello") - def test_fcall_ro(self, unstable_r): - unstable_r.function_load("Lua", "mylib", function) - assert unstable_r.fcall_ro("myfunc", 0, "hello") == "hello" - unstable_r.function_load("Lua", "mylib2", set_function) + def test_fcall_ro(self, r): + r.function_load("Lua", "mylib", function) + assert r.fcall_ro("myfunc", 0, "hello") == "hello" + r.function_load("Lua", "mylib2", set_function) with pytest.raises(ResponseError): - unstable_r.fcall_ro("set", 1, "foo", "bar") + r.fcall_ro("set", 1, "foo", "bar") - def test_function_dump_restore(self, unstable_r): - unstable_r.function_load("Lua", "mylib", set_function) - payload = unstable_r.function_dump() - assert unstable_r.fcall("set", 1, "foo", "bar") == "OK" - unstable_r.function_delete("mylib") + def test_function_dump_restore(self, r): + r.function_load("Lua", "mylib", set_function) + payload = r.function_dump() + assert r.fcall("set", 1, "foo", "bar") == "OK" + r.function_delete("mylib") with pytest.raises(ResponseError): - unstable_r.fcall("set", 1, "foo", "bar") - assert unstable_r.function_restore(payload) - assert unstable_r.fcall("set", 1, "foo", "bar") == "OK" - unstable_r.function_load("Lua", "mylib2", get_function) - assert unstable_r.fcall("get", 1, "foo") == "bar" - unstable_r.function_delete("mylib") - assert unstable_r.function_restore(payload, "FLUSH") + r.fcall("set", 1, "foo", "bar") + assert r.function_restore(payload) + assert r.fcall("set", 1, "foo", "bar") == "OK" + r.function_load("Lua", "mylib2", get_function) + assert r.fcall("get", 1, "foo") == "bar" + r.function_delete("mylib") + assert r.function_restore(payload, "FLUSH") with pytest.raises(ResponseError): - unstable_r.fcall("get", 1, "foo") + r.fcall("get", 1, "foo") diff --git a/tox.ini b/tox.ini index d4f15acd50..e9174a66c5 100644 --- a/tox.ini +++ b/tox.ini @@ -283,10 +283,11 @@ extras = ocsp: cryptography, pyopenssl, requests setenv = CLUSTER_URL = "redis://localhost:16379/0" + UNSTABLE_CLUSTER_URL = "redis://localhost:6372/0" commands = standalone: pytest --cov=./ --cov-report=xml:coverage_redis.xml -W always -m 'not onlycluster' {posargs} standalone-uvloop: pytest --cov=./ --cov-report=xml:coverage_redis.xml -W always -m 'not onlycluster' --uvloop {posargs} - cluster: pytest --cov=./ --cov-report=xml:coverage_cluster.xml -W always -m 'not onlynoncluster and not redismod' --redis-url={env:CLUSTER_URL:} --redismod-url={env:CLUSTER_URL:} {posargs} + cluster: pytest --cov=./ --cov-report=xml:coverage_cluster.xml -W always -m 'not onlynoncluster and not redismod' --redis-url={env:CLUSTER_URL:} --redis-unstable-url={env:UNSTABLE_CLUSTER_URL:} {posargs} cluster-uvloop: pytest --cov=./ --cov-report=xml:coverage_redis.xml -W always -m 'not onlycluster' --uvloop {posargs} [testenv:redis5] From d323b926d80612d540a96513258cf6320fb47d82 Mon Sep 17 00:00:00 2001 From: dvora-h Date: Mon, 7 Mar 2022 00:35:31 +0200 Subject: [PATCH 6/6] skip test --- tests/test_commands.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_commands.py b/tests/test_commands.py index c9b04669ea..f1651203b6 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -4239,10 +4239,10 @@ def test_command_count(self, r): assert isinstance(res, int) assert res >= 100 - # @skip_if_server_versiov_lt("7.0.0") - def test_command_docs(self, unstable_r): + @skip_if_server_version_lt("7.0.0") + def test_command_docs(self, r): with pytest.raises(NotImplementedError): - unstable_r.command_docs("set") + r.command_docs("set") @pytest.mark.onlynoncluster @skip_if_server_version_lt("2.8.13")