From 06cba6ccd165ca8b224797e37fccb9e63f026d77 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 21 Mar 2020 11:28:17 +0100 Subject: Adding upstream version 1.9.1. Signed-off-by: Daniel Baumann --- tests/__init__.py | 0 tests/cli_tests/test_cli_pubsub.py | 31 ++ tests/cli_tests/test_cli_start.py | 75 +++ tests/cli_tests/test_command_input.py | 55 +++ tests/cli_tests/test_completer.py | 51 +++ tests/cli_tests/test_config.py | 25 + tests/cli_tests/test_dsn.py | 53 +++ tests/cli_tests/test_history.py | 42 ++ tests/cli_tests/test_on_error.py | 12 + tests/cli_tests/test_pager.py | 117 +++++ tests/cli_tests/test_self_implemented_command.py | 14 + tests/cli_tests/test_shell_pipeline.py | 11 + tests/cli_tests/test_string_execute.py | 39 ++ tests/cli_tests/test_transaction.py | 70 +++ tests/cli_tests/test_warning.py | 51 +++ tests/cli_tests/wrappager.py | 14 + tests/conftest.py | 137 ++++++ tests/unittests/command_parse/test_base_token.py | 56 +++ tests/unittests/command_parse/test_cluster.py | 227 ++++++++++ tests/unittests/command_parse/test_connection.py | 97 ++++ .../unittests/command_parse/test_generic_parse.py | 179 ++++++++ tests/unittests/command_parse/test_geo.py | 39 ++ tests/unittests/command_parse/test_hash_parse.py | 45 ++ tests/unittests/command_parse/test_hll_parse.py | 5 + tests/unittests/command_parse/test_list_parse.py | 108 +++++ tests/unittests/command_parse/test_pubsub.py | 15 + tests/unittests/command_parse/test_script.py | 22 + tests/unittests/command_parse/test_server.py | 201 ++++++++ tests/unittests/command_parse/test_set_parse.py | 40 ++ .../command_parse/test_sorted_set_parse.py | 172 +++++++ tests/unittests/command_parse/test_stream.py | 459 +++++++++++++++++++ tests/unittests/command_parse/test_string_parse.py | 345 ++++++++++++++ tests/unittests/test_client.py | 504 +++++++++++++++++++++ tests/unittests/test_completers.py | 356 +++++++++++++++ tests/unittests/test_entry.py | 272 +++++++++++ tests/unittests/test_markdown_doc_render.py | 28 ++ tests/unittests/test_render_functions.py | 486 ++++++++++++++++++++ tests/unittests/test_utils.py | 122 +++++ 38 files changed, 4575 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/cli_tests/test_cli_pubsub.py create mode 100644 tests/cli_tests/test_cli_start.py create mode 100644 tests/cli_tests/test_command_input.py create mode 100644 tests/cli_tests/test_completer.py create mode 100644 tests/cli_tests/test_config.py create mode 100644 tests/cli_tests/test_dsn.py create mode 100644 tests/cli_tests/test_history.py create mode 100644 tests/cli_tests/test_on_error.py create mode 100644 tests/cli_tests/test_pager.py create mode 100644 tests/cli_tests/test_self_implemented_command.py create mode 100644 tests/cli_tests/test_shell_pipeline.py create mode 100644 tests/cli_tests/test_string_execute.py create mode 100644 tests/cli_tests/test_transaction.py create mode 100644 tests/cli_tests/test_warning.py create mode 100644 tests/cli_tests/wrappager.py create mode 100644 tests/conftest.py create mode 100644 tests/unittests/command_parse/test_base_token.py create mode 100644 tests/unittests/command_parse/test_cluster.py create mode 100644 tests/unittests/command_parse/test_connection.py create mode 100644 tests/unittests/command_parse/test_generic_parse.py create mode 100644 tests/unittests/command_parse/test_geo.py create mode 100644 tests/unittests/command_parse/test_hash_parse.py create mode 100644 tests/unittests/command_parse/test_hll_parse.py create mode 100644 tests/unittests/command_parse/test_list_parse.py create mode 100644 tests/unittests/command_parse/test_pubsub.py create mode 100644 tests/unittests/command_parse/test_script.py create mode 100644 tests/unittests/command_parse/test_server.py create mode 100644 tests/unittests/command_parse/test_set_parse.py create mode 100644 tests/unittests/command_parse/test_sorted_set_parse.py create mode 100644 tests/unittests/command_parse/test_stream.py create mode 100644 tests/unittests/command_parse/test_string_parse.py create mode 100644 tests/unittests/test_client.py create mode 100644 tests/unittests/test_completers.py create mode 100644 tests/unittests/test_entry.py create mode 100644 tests/unittests/test_markdown_doc_render.py create mode 100644 tests/unittests/test_render_functions.py create mode 100644 tests/unittests/test_utils.py (limited to 'tests') diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cli_tests/test_cli_pubsub.py b/tests/cli_tests/test_cli_pubsub.py new file mode 100644 index 0000000..956e105 --- /dev/null +++ b/tests/cli_tests/test_cli_pubsub.py @@ -0,0 +1,31 @@ +def test_subscribe(cli, clean_redis): + cli.sendline("subscribe foo") + cli.expect("subscribe from") + cli.expect("foo") + cli.expect("1") + + clean_redis.publish("foo", "test message") + cli.expect("from") + cli.expect("foo") + cli.expect("test message") + + # unsubscribe, send ctrl-c + cli.send(chr(3)) + cli.expect("unsubscribe from") + cli.expect("0") + + +def test_subscribe_in_raw_mode(raw_cli, clean_redis): + raw_cli.sendline("subscribe foo") + raw_cli.expect("subscribe\r") + raw_cli.expect("foo\r") + raw_cli.expect("1\r") + + clean_redis.publish("foo", "test message") + raw_cli.expect("message\r") + raw_cli.expect("foo\r") + raw_cli.expect("test message") + + # unsubscribe, send ctrl-c + raw_cli.send(chr(3)) + raw_cli.expect("0\r") diff --git a/tests/cli_tests/test_cli_start.py b/tests/cli_tests/test_cli_start.py new file mode 100644 index 0000000..d33185a --- /dev/null +++ b/tests/cli_tests/test_cli_start.py @@ -0,0 +1,75 @@ +import pexpect +import pytest +from textwrap import dedent + + +def test_start_on_connection_error(): + cli = pexpect.spawn("iredis -p 12345", timeout=1) + cli.logfile_read = open("cli_test.log", "ab") + cli.expect(r"Error \d+ connecting to 127.0.0.1:12345. Connection refused.") + cli.close() + + +def test_short_help_option(config): + c = pexpect.spawn("iredis -h", timeout=2) + + c.expect("Show this message and exit.") + + c = pexpect.spawn("iredis -h 127.0.0.1") + c.expect("127.0.0.1:6379>") + + c.close() + + +@pytest.mark.skipif("int(os.environ['REDIS_VERSION']) != 5") +def test_server_version_in_starting_on5(): + c = pexpect.spawn("iredis", timeout=2) + c.expect("redis-server 5") + c.close() + + +@pytest.mark.skipif("int(os.environ['REDIS_VERSION']) != 6") +def test_server_version_in_starting_on6(): + c = pexpect.spawn("iredis", timeout=2) + c.expect("redis-server 6") + c.close() + + +def test_connection_using_url(clean_redis): + c = pexpect.spawn("iredis --url redis://localhost:6379/7", timeout=2) + c.logfile_read = open("cli_test.log", "ab") + c.expect(["iredis", "127.0.0.1:6379[7]>"]) + c.sendline("set current-db 7") + c.expect("OK") + c.close() + + +def test_connection_using_url_from_env(clean_redis, monkeypatch): + monkeypatch.setenv("IREDIS_URL", "redis://localhost:6379/7") + c = pexpect.spawn("iredis", timeout=2) + c.logfile_read = open("cli_test.log", "ab") + c.expect(["iredis", "localhost:6379[7]>"]) + c.sendline("set current-db 7") + c.expect("OK") + c.close() + + +@pytest.mark.xfail(reason="current test in github action, socket not supported.") +# https://github.community/t5/GitHub-Actions/Job-service-command/td-p/33901# +# https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idservices +def test_connect_via_socket(fake_redis_socket): + config_content = dedent( + """ + [main] + log_location = /tmp/iredis1.log + no_info=True + """ + ) + with open("/tmp/iredisrc", "w+") as etc_config: + etc_config.write(config_content) + + c = pexpect.spawn("iredis --iredisrc /tmp/iredisrc -s /tmp/test.sock", timeout=2) + c.logfile_read = open("cli_test.log", "ab") + c.expect("redis /tmp/test.sock") + + c.close() diff --git a/tests/cli_tests/test_command_input.py b/tests/cli_tests/test_command_input.py new file mode 100644 index 0000000..4f970f5 --- /dev/null +++ b/tests/cli_tests/test_command_input.py @@ -0,0 +1,55 @@ +import pytest + + +def test_wrong_select_db_index(cli): + cli.sendline("select 1") + cli.expect(["OK", "127.0.0.1"]) + + cli.sendline("select 128") + cli.expect(["DB index is out of range", "127.0.0.1:6379[1]>"]) + + cli.sendline("select abc") + cli.expect(["invalid DB index", "127.0.0.1:6379[1]>"]) + + cli.sendline("select 15") + cli.expect("OK") + + +def test_set_command_with_shash(clean_redis, cli): + cli.sendline("set a \\hello\\") # legal redis command + cli.expect("OK") + + cli.sendline("get a") + cli.expect(r"hello") + + +def test_enter_key_binding(clean_redis, cli): + cli.send("set") + cli.expect("set") + cli.send("\033[B") # down + cli.sendline() # enter + + cli.sendline(" a 'hello'") + cli.expect("OK") + + cli.sendline("get a") + cli.expect(r"hello") + + +@pytest.mark.skipif("int(os.environ['REDIS_VERSION']) < 6") +def test_auth_hidden_password_with_username(clean_redis, cli): + cli.send("auth default hello-world") + cli.expect("default") + cli.expect(r"\*{11}") + + +@pytest.mark.skipif("int(os.environ['REDIS_VERSION']) > 5") +def test_auth_hidden_password(clean_redis, cli): + cli.send("auth hello-world") + cli.expect("auth") + cli.expect(r"\*{11}") + + +def test_hello_command_is_not_supported(cli): + cli.sendline("hello 3") + cli.expect("IRedis currently not support RESP3") diff --git a/tests/cli_tests/test_completer.py b/tests/cli_tests/test_completer.py new file mode 100644 index 0000000..4ffd058 --- /dev/null +++ b/tests/cli_tests/test_completer.py @@ -0,0 +1,51 @@ +import pytest + + +def test_integer_type_completer(cli): + cli.expect("127.0.0.1") + cli.send("BITFIELD meykey GET ") + cli.expect(["i64", "u63", "u62"]) + cli.sendline("u4 #0") + cli.expect("127.0.0.1") + + cli.send("BITFIELD meykey GET ") + cli.expect(["u4", "i64", "u63", "u62"]) + + +def test_command_completion_when_a_command_is_another_command_substring( + cli, clean_redis +): + cli.expect("127.0.0.1") + cli.send("set") + cli.expect(["set", "setnx", "setex", "setbit", "setrange"]) + + cli.send("n") + cli.expect("setnx") + cli.send("x") + cli.expect("setnx") + cli.sendline("foo bar") + cli.expect(["1", "127.0.0.1"]) + + cli.send("setnx") + cli.expect("foo") + + +def test_command_completion_when_space_command(cli, clean_redis): + cli.expect("127.0.0.1") + + cli.send("command in") + cli.expect("command info") + + +@pytest.mark.skipif("int(os.environ['REDIS_VERSION']) < 6") +def test_username_completer(cli, iredis_client): + iredis_client.execute("acl setuser", "foo1") + iredis_client.execute("acl setuser", "bar2") + + cli.expect("127.0.0.1") + cli.sendline("acl users") + cli.expect("foo1") + + cli.send("acl deluser ") + cli.expect("foo1") + cli.expect("bar2") diff --git a/tests/cli_tests/test_config.py b/tests/cli_tests/test_config.py new file mode 100644 index 0000000..7d6839d --- /dev/null +++ b/tests/cli_tests/test_config.py @@ -0,0 +1,25 @@ +import pexpect +from textwrap import dedent +from pathlib import Path + + +def test_log_location_config(): + config_content = dedent( + """ + [main] + log_location = /tmp/iredis1.log + """ + ) + with open("/tmp/iredisrc", "w+") as etc_config: + etc_config.write(config_content) + + cli = pexpect.spawn("iredis -n 15 --iredisrc /tmp/iredisrc", timeout=1) + cli.expect("127.0.0.1") + cli.close() + + log = Path("/tmp/iredis1.log") + assert log.exists() + with open(log, "r") as logfile: + content = logfile.read() + + assert len(content) > 100 diff --git a/tests/cli_tests/test_dsn.py b/tests/cli_tests/test_dsn.py new file mode 100644 index 0000000..8c9528d --- /dev/null +++ b/tests/cli_tests/test_dsn.py @@ -0,0 +1,53 @@ +import os + +import pexpect +from textwrap import dedent +import pytest + + +def test_using_dsn(): + config_content = dedent( + """ + [alias_dsn] + local = redis://localhost:6379/15 + """ + ) + with open("/tmp/iredisrc", "w+") as etc_config: + etc_config.write(config_content) + + cli = pexpect.spawn("iredis --iredisrc /tmp/iredisrc --dsn local", timeout=1) + cli.logfile_read = open("cli_test.log", "ab") + cli.expect(["iredis", "localhost:6379[15]>"]) + cli.close() + + # overwrite with -n + cli = pexpect.spawn("iredis --iredisrc /tmp/iredisrc --dsn local -n 3", timeout=1) + cli.logfile_read = open("cli_test.log", "ab") + cli.expect(["iredis", "localhost:6379[3]>"]) + cli.close() + + # dsn not exists + cli = pexpect.spawn("iredis --iredisrc /tmp/iredisrc --dsn ghost-dsn", timeout=1) + cli.expect(["Could not find the specified DSN in the config file."]) + cli.close() + assert cli.status == 1 + + +@pytest.mark.skipif( + not os.path.exists("/tmp/redis/redis.sock"), reason="unix socket is not found" +) +def test_using_dsn_unix(): + config_content = dedent( + """ + [alias_dsn] + unix = unix:///tmp/redis/redis.sock?db=3 + """ + ) + with open("/tmp/iredisrc", "w+") as etc_config: + etc_config.write(config_content) + + cli = pexpect.spawn("iredis --iredisrc /tmp/iredisrc --dsn unix", timeout=2) + cli.logfile_read = open("cli_test.log", "ab") + cli.expect(["iredis", "redis /tmp/redis/redis.sock[3]>"]) + + cli.close() diff --git a/tests/cli_tests/test_history.py b/tests/cli_tests/test_history.py new file mode 100644 index 0000000..09e6c82 --- /dev/null +++ b/tests/cli_tests/test_history.py @@ -0,0 +1,42 @@ +import os +import pexpect +from pathlib import Path +from textwrap import dedent + + +def test_history_not_log_auth(cli): + cli.sendline("AUTH 123") + cli.expect(["Client sent AUTH, but no password is set", "127.0.0.1"]) + cli.sendline("set foo bar") + cli.expect("OK") + + with open(os.path.expanduser("~/.iredis_history"), "r") as history_file: + content = history_file.read() + + assert "set foo bar" in content + assert "AUTH" not in content + + +def test_history_create_and_writing_with_config(): + config_content = dedent( + """ + [main] + history_location = /tmp/iredis_history.txt + """ + ) + with open("/tmp/iredisrc", "w+") as etc_config: + etc_config.write(config_content) + + cli = pexpect.spawn("iredis -n 15 --iredisrc /tmp/iredisrc", timeout=2) + cli.expect("127.0.0.1") + cli.sendline("set hello world") + cli.expect("OK") + cli.close() + + log = Path("/tmp/iredis_history.txt") + assert log.exists() + + with open(log, "r") as logfile: + content = logfile.read() + + assert "set hello world" in content diff --git a/tests/cli_tests/test_on_error.py b/tests/cli_tests/test_on_error.py new file mode 100644 index 0000000..9ab8927 --- /dev/null +++ b/tests/cli_tests/test_on_error.py @@ -0,0 +1,12 @@ +def test_wrong_stream_type(clean_redis, cli): + clean_redis.lpush("mylist", "foo") + cli.sendline("xrange mylist 0 -1") + cli.expect("error") + cli.expect("Invalid stream ID specified as stream command argument") + + +def test_wrong_stream_type_in_raw_mode(clean_redis, raw_cli): + clean_redis.lpush("mylist", "foo") + raw_cli.sendline("xrange mylist 0 -1") + raw_cli.expect("ERROR") + raw_cli.expect("Invalid stream ID specified as stream command argument") diff --git a/tests/cli_tests/test_pager.py b/tests/cli_tests/test_pager.py new file mode 100644 index 0000000..38ced31 --- /dev/null +++ b/tests/cli_tests/test_pager.py @@ -0,0 +1,117 @@ +# noqa: F541 +import os +import sys +import pexpect +import pathlib +from contextlib import contextmanager +from textwrap import dedent + + +TEST_IREDISRC = "/tmp/.iredisrc.test" +TEST_PAGER_BOUNDARY = "---boundary---" +TEST_PAGER_BOUNDARY_NUMBER = "---88938347271---" + +env_pager = "{0} {1} {2}".format( + sys.executable, + os.path.join(pathlib.Path(__file__).parent, "wrappager.py"), + TEST_PAGER_BOUNDARY, +) +env_pager_numbers = "{0} {1} {2}".format( + sys.executable, + os.path.join(pathlib.Path(__file__).parent, "wrappager.py"), + TEST_PAGER_BOUNDARY_NUMBER, +) + + +@contextmanager +def pager_enabled_cli(): + env = os.environ + env["PAGER"] = env_pager + child = pexpect.spawn("iredis -n 15", timeout=3, env=env) + child.logfile_read = open("cli_test.log", "ab") + child.expect("127.0.0.1") + try: + yield child + finally: + child.close() + + +def test_using_pager_when_rows_too_high(clean_redis): + for index in range(100): + clean_redis.lpush("long-list", f"value-{index}") + with pager_enabled_cli() as child: + child.sendline("lrange long-list 0 -1") + child.expect(TEST_PAGER_BOUNDARY) + child.expect("value-1") + child.expect(TEST_PAGER_BOUNDARY) + + +def test_using_pager_works_for_help(): + with pager_enabled_cli() as child: + child.sendline("help set") + child.expect(TEST_PAGER_BOUNDARY) + child.expect("Set the string value of a key") + child.expect(TEST_PAGER_BOUNDARY) + + +def test_pager_works_for_peek(clean_redis): + for index in range(100): + clean_redis.lpush("long-list", f"value-{index}") + with pager_enabled_cli() as child: + child.sendline("peek long-list") + child.expect(TEST_PAGER_BOUNDARY) + child.expect("(quicklist)") + child.expect("value-1") + child.expect(TEST_PAGER_BOUNDARY) + + +def test_using_pager_from_config(clean_redis): + config_content = dedent( + f""" + [main] + log_location = /tmp/iredis1.log + pager = {env_pager_numbers} + """ + ) + + with open(TEST_IREDISRC, "w+") as test_iredisrc: + test_iredisrc.write(config_content) + + child = pexpect.spawn(f"iredis -n 15 --iredisrc {TEST_IREDISRC}", timeout=3) + child.logfile_read = open("cli_test.log", "ab") + child.expect("127.0.0.1") + for index in range(100): + clean_redis.lpush("long-list", f"value-{index}") + child.sendline("lrange long-list 0 -1") + child.expect(TEST_PAGER_BOUNDARY_NUMBER) + child.expect("value-1") + child.expect(TEST_PAGER_BOUNDARY_NUMBER) + child.close() + + +def test_using_pager_from_config_when_env_config_both_set(clean_redis): + config_content = dedent( + f""" + [main] + log_location = /tmp/iredis1.log + pager = {env_pager_numbers} + """ + ) + + with open(TEST_IREDISRC, "w+") as test_iredisrc: + test_iredisrc.write(config_content) + + env = os.environ + env["PAGER"] = env_pager + child = pexpect.spawn( + f"iredis -n 15 --iredisrc {TEST_IREDISRC}", timeout=3, env=env + ) + child.logfile_read = open("cli_test.log", "ab") + child.expect("127.0.0.1") + for index in range(100): + clean_redis.lpush("long-list", f"value-{index}") + child.sendline("lrange long-list 0 -1") + child.expect(TEST_PAGER_BOUNDARY_NUMBER) + child.expect("value-1") + child.expect(TEST_PAGER_BOUNDARY_NUMBER) + child.close() diff --git a/tests/cli_tests/test_self_implemented_command.py b/tests/cli_tests/test_self_implemented_command.py new file mode 100644 index 0000000..328d56b --- /dev/null +++ b/tests/cli_tests/test_self_implemented_command.py @@ -0,0 +1,14 @@ +""" +check ascii table: +http://ascii-table.com/ansi-escape-sequences.php +""" + + +def test_clear(cli): + cli.sendline("clear") + cli.expect("\\[2J") # clear screen + + +def test_exirt(cli): + cli.sendline("EXIT") + cli.expect("Goodbye!") diff --git a/tests/cli_tests/test_shell_pipeline.py b/tests/cli_tests/test_shell_pipeline.py new file mode 100644 index 0000000..4bacf8d --- /dev/null +++ b/tests/cli_tests/test_shell_pipeline.py @@ -0,0 +1,11 @@ +import pexpect + + +def test_running_disable_shell_pipeline(): + cli = pexpect.spawn("iredis -n 15 --no-shell", timeout=2) + cli.expect("127.0.0.1") + cli.sendline("set foo hello") + cli.expect("OK") + cli.sendline("get foo | grep w") + cli.expect(r"hello") + cli.close() diff --git a/tests/cli_tests/test_string_execute.py b/tests/cli_tests/test_string_execute.py new file mode 100644 index 0000000..310adab --- /dev/null +++ b/tests/cli_tests/test_string_execute.py @@ -0,0 +1,39 @@ +def test_set(cli): + cli.sendline("set foo bar") + cli.expect(["OK", "127.0.0.1"]) + + cli.sendline("set foo bar nx") + cli.expect(["(nil)", "127.0.0.1"]) + + cli.sendline("set foo bar xx") + cli.expect(["OK", "127.0.0.1"]) + + cli.sendline("set foo1 bar xx") + cli.expect(["(nil)", "127.0.0.1"]) + + +def test_get(cli): + cli.sendline("set foo bar") + cli.expect("OK") + + cli.sendline("get foo") + cli.expect('"bar"') + + +def test_delete_string(clean_redis, cli, config): + config.warning = True + cli.sendline("set foo bar") + cli.expect("OK") + cli.sendline("del foo") + cli.expect("Do you want to proceed") + cli.sendline("yes") + cli.expect("1") + + cli.sendline("get foo") + cli.expect("(nil)") + + +def test_on_dangerous_commands(cli, config): + config.warning = True + cli.sendline("keys *") + cli.expect("KEYS will hang redis server, use SCAN instead") diff --git a/tests/cli_tests/test_transaction.py b/tests/cli_tests/test_transaction.py new file mode 100644 index 0000000..b5c6b4e --- /dev/null +++ b/tests/cli_tests/test_transaction.py @@ -0,0 +1,70 @@ +import pytest +import pexpect + + +def test_trasaction_rprompt(clean_redis, cli): + cli.sendline("multi") + cli.expect(["OK", "transaction", "127.0.0.1"]) + + cli.sendline("get foo") + cli.expect(["QUEUED", "127.0.0.1", "transaction"]) + + cli.sendline("set hello world") + cli.expect(["QUEUED", "127.0.0.1", "transaction"]) + + cli.sendline("ping") + cli.expect(["QUEUED", "127.0.0.1", "transaction"]) + + cli.sendline("EXEC") + cli.expect("(nil)") + cli.expect("OK") + cli.expect("PONG") + cli.expect("127.0.0.1") + + with pytest.raises(pexpect.exceptions.TIMEOUT): + cli.expect("transaction") + + +def test_trasaction_syntax_error(cli): + cli.sendline("multi") + cli.sendline("get foo 1") + cli.expect(["wrong number of arguments for 'get' command", "transaction"]) + + cli.sendline("EXEC") + cli.expect("Transaction discarded because of previous errors.") + with pytest.raises(pexpect.exceptions.TIMEOUT): + cli.expect("transaction") + + +def test_trasaction_in_raw_mode(clean_redis, raw_cli): + clean_redis.set("foo", "bar") + + raw_cli.sendline("multi") + raw_cli.expect(["OK", "transaction", "127.0.0.1"]) + + raw_cli.sendline("get foo") + raw_cli.expect(["QUEUED", "127.0.0.1", "transaction"]) + + raw_cli.sendline("EXEC") + raw_cli.expect("bar") + raw_cli.expect("127.0.0.1") + + with pytest.raises(pexpect.exceptions.TIMEOUT): + raw_cli.expect("transaction") + + +def test_exec_return_nil_when_using_watch(clean_redis, cli): + cli.sendline("watch foo") + cli.expect("OK") + + cli.sendline("multi") + cli.expect("OK") + + cli.sendline("get bar") + cli.expect("QUEUED") + + # transaction will fail, return nil + clean_redis.set("foo", "hello!") + + cli.sendline("exec") + cli.expect("(nil)") diff --git a/tests/cli_tests/test_warning.py b/tests/cli_tests/test_warning.py new file mode 100644 index 0000000..402c51b --- /dev/null +++ b/tests/cli_tests/test_warning.py @@ -0,0 +1,51 @@ +from iredis.warning import is_dangerous + + +def test_is_dangerous(): + assert is_dangerous("KEYS") == ( + True, + "KEYS will hang redis server, use SCAN instead", + ) + + +def test_warning_for_dangerous_command(cli): + cli.sendline("config set save '900 1'") + cli.expect("Do you want to proceed?") + cli.sendline("yes") + + cli.sendline("config get save") + cli.expect("900 1") + + +def test_warnings_in_raw_mode(clean_redis, raw_cli): + clean_redis.set("foo", "bar") + raw_cli.sendline("keys *") + raw_cli.expect("Do you want to proceed?") + raw_cli.sendline("y") + raw_cli.expect("foo") + + +def test_warnings_in_raw_mode_canceled(clean_redis, raw_cli): + clean_redis.set("foo", "bar") + raw_cli.sendline("keys *") + raw_cli.expect("Do you want to proceed?") + raw_cli.sendline("n") + # the f should never appeared + raw_cli.expect("Canceled![^f]+127.0.0.1") + + +def test_warnings_confirmed(clean_redis, cli): + clean_redis.set("foo", "bar") + cli.sendline("keys *") + cli.expect("Do you want to proceed?") + cli.sendline("y") + cli.expect("foo") + + +def test_warnings_canceled(clean_redis, cli): + clean_redis.set("foo", "bar") + cli.sendline("keys *") + cli.expect("Do you want to proceed?") + cli.sendline("n") + # the f should never appeared + cli.expect("Canceled![^f]+127.0.0.1") diff --git a/tests/cli_tests/wrappager.py b/tests/cli_tests/wrappager.py new file mode 100644 index 0000000..dce86ce --- /dev/null +++ b/tests/cli_tests/wrappager.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +import sys +import fileinput + + +def wrappager(boundary): + print(boundary) + for line in fileinput.input(files="-"): + sys.stdout.write(line) + print(boundary) + + +if __name__ == "__main__": + wrappager(sys.argv[1]) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..4d7d643 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,137 @@ +import os +import tempfile +from textwrap import dedent + +import pexpect +import pytest +import redis + +from iredis.client import Client +from iredis.commands import split_command_args +from iredis.redis_grammar import get_command_grammar +from iredis.exceptions import InvalidArguments +from iredis.config import Config, config as global_config + + +TIMEOUT = 2 +HISTORY_FILE = ".iredis_history" + + +@pytest.fixture +def judge_command(): + def judge_command_func(input_text, expect): + if expect == "invalid": + with pytest.raises(InvalidArguments): + split_command_args(input_text) + return + + command, _ = split_command_args(input_text) + grammar = get_command_grammar(command) + + m = grammar.match(input_text) + + # test on not match + if not expect: + assert m is None + return + + variables = m.variables() + print("Found variables: {}".format(variables)) + for expect_token, expect_value in expect.items(): + all_variables = variables.getall(expect_token) + if len(all_variables) > 1: + assert sorted(all_variables) == sorted(expect_value) + else: + assert variables.get(expect_token) == expect_value + + return judge_command_func + + +@pytest.fixture(scope="function") +def clean_redis(): + """ + Return a empty redis db. (redis-py client) + """ + client = redis.StrictRedis(db=15) + client.flushdb() + return client + + +@pytest.fixture +def iredis_client(): + return Client("127.0.0.1", "6379", db=15) + + +@pytest.fixture +def config(): + newconfig = Config() + global_config.__dict__ = newconfig.__dict__ + config.raw = False + return global_config + + +@pytest.fixture(scope="function") +def cli(): + """Open iredis subprocess to test""" + f = tempfile.TemporaryFile("w") + config_content = dedent( + """ + [main] + log_location = + warning = True + """ + ) + f.write(config_content) + f.close() + env = os.environ + env["PROMPT_TOOLKIT_NO_CPR"] = "1" + + child = pexpect.spawn(f"iredis -n 15 --iredisrc {f.name}", timeout=TIMEOUT, env=env) + child.logfile_read = open("cli_test.log", "ab") + child.expect(["https://iredis.io/issues", "127.0.0.1"]) + yield child + child.close() + + +@pytest.fixture(scope="function") +def raw_cli(): + """Open iredis subprocess to test""" + TEST_IREDISRC = "/tmp/.iredisrc.test" + config_content = dedent( + """ + [main] + log_location = + warning = True + """ + ) + + with open(TEST_IREDISRC, "w+") as test_iredisrc: + test_iredisrc.write(config_content) + + child = pexpect.spawn( + f"iredis --raw -n 15 --iredisrc {TEST_IREDISRC}", timeout=TIMEOUT + ) + child.logfile_read = open("cli_test.log", "ab") + child.expect(["https://iredis.io/issues", "127.0.0.1"]) + yield child + child.close() + + +@pytest.fixture(scope="function") +def cli_without_warning(): + f = tempfile.TemporaryFile("w") + config_content = dedent( + """ + [main] + log_location = /tmp/iredis1.log + warning = False + """ + ) + f.write(config_content) + f.close() + + cli = pexpect.spawn(f"iredis -n 15 --iredisrc {f.name}", timeout=1) + cli.logfile_read = open("cli_test.log", "ab") + yield cli + cli.close() + os.remove("/tmp/iredisrc") diff --git a/tests/unittests/command_parse/test_base_token.py b/tests/unittests/command_parse/test_base_token.py new file mode 100644 index 0000000..b04722e --- /dev/null +++ b/tests/unittests/command_parse/test_base_token.py @@ -0,0 +1,56 @@ +def test_ip_match(judge_command): + def ip_valid(ip, valid): + if valid: + judge_command( + f"cluster meet {ip} 6379", + {"command": "cluster meet", "ip": ip, "port": "6379"}, + ) + else: + judge_command(f"cluster meet {ip} 6379", None) + + ip_valid("192.168.0.1", True) + ip_valid("255.255.255.255", True) + ip_valid("192.168.0.256", False) + ip_valid("192.256.0.26", False) + ip_valid("192.255.256.26", False) + ip_valid("0.0.0.0", True) + ip_valid("99.999.100.1", False) + ip_valid("300.168.0.1", False) + + +def test_port_match(judge_command): + def port_valid(port, valid): + if valid: + judge_command( + f"cluster meet 192.168.0.1 {port}", + {"command": "cluster meet", "ip": "192.168.0.1", "port": port}, + ) + else: + judge_command(f"cluster meet 192.168.0.1 {port}", None) + + port_valid("65535", True) + port_valid("0", False) + port_valid("1", True) + port_valid("192.168.0.256", False) + port_valid("65536", False) + port_valid("65545", False) + port_valid("65635", False) + port_valid("66535", False) + port_valid("75535", False) + port_valid("1024", True) + port_valid("6553", True) + port_valid("99999", False) + port_valid("99999999", False) + + +def test_command_with_key_in_quotes(judge_command): + judge_command( + 'cluster keyslot "mykey"', {"command": "cluster keyslot", "key": '"mykey"'} + ) + judge_command( + 'cluster keyslot "\\"mykey"', + {"command": "cluster keyslot", "key": '"\\"mykey"'}, + ) + judge_command( + 'cluster keyslot "mykey "', {"command": "cluster keyslot", "key": '"mykey "'} + ) diff --git a/tests/unittests/command_parse/test_cluster.py b/tests/unittests/command_parse/test_cluster.py new file mode 100644 index 0000000..e60fe92 --- /dev/null +++ b/tests/unittests/command_parse/test_cluster.py @@ -0,0 +1,227 @@ +""" +redis command in `cluster` group parse test. +""" + + +def test_command_cluster_addslots(judge_command): + judge_command("cluster addslots 1", {"command": "cluster addslots", "slots": "1"}) + judge_command("CLUSTER ADDSLOTS 1", {"command": "CLUSTER ADDSLOTS", "slots": "1"}) + judge_command( + "cluster addslots 1 2 3 4", {"command": "cluster addslots", "slots": "1 2 3 4"} + ) + judge_command("cluster addslots 1 a", None) + judge_command("cluster addslots a", None) + judge_command("cluster addslots a 4", None) + judge_command("cluster addslots abc", None) + + +def test_command_cluster_count_failure_reports(judge_command): + judge_command( + "cluster count-failure-reports 1", + {"command": "cluster count-failure-reports", "node": "1"}, + ) + judge_command( + "CLUSTER COUNT-FAILURE-REPORTS 1", + {"command": "CLUSTER COUNT-FAILURE-REPORTS", "node": "1"}, + ) + judge_command("cluster count-failure-reports 1 2 3 4", None) + judge_command("cluster count-failure-reports 1 a", None) + judge_command("cluster count-failure-reports a", None) + judge_command("cluster count-failure-reports a 2", None) + judge_command("cluster count-failure-reports abc", None) + + +def test_command_cluster_countkeysinslot(judge_command): + judge_command( + "cluster countkeysinslot 1", {"command": "cluster countkeysinslot", "slot": "1"} + ) + judge_command( + "CLUSTER COUNTKEYSINSLOT 1", {"command": "CLUSTER COUNTKEYSINSLOT", "slot": "1"} + ) + judge_command("cluster countkeysinslot 1 2 3 4", None) + judge_command("cluster countkeysinslot 1 a", None) + judge_command("cluster countkeysinslot a", None) + judge_command("cluster countkeysinslot a 4", None) + judge_command("cluster countkeysinslot abc", None) + + +def test_command_cluster_delslots(judge_command): + judge_command("cluster delslots 1", {"command": "cluster delslots", "slots": "1"}) + judge_command("CLUSTER DELSLOTS 1", {"command": "CLUSTER DELSLOTS", "slots": "1"}) + judge_command( + "cluster delslots 1 2 3 4", {"command": "cluster delslots", "slots": "1 2 3 4"} + ) + judge_command("cluster delslots 1 a", None) + judge_command("cluster delslots a", None) + judge_command("cluster delslots a 4", None) + judge_command("cluster delslots abc", None) + + +def test_command_cluster_failover(judge_command): + judge_command( + "cluster failover force", + {"command": "cluster failover", "failoverchoice": "force"}, + ) + judge_command( + "cluster failover takeover", + {"command": "cluster failover", "failoverchoice": "takeover"}, + ) + judge_command( + "CLUSTER FAILOVER FORCE", + {"command": "CLUSTER FAILOVER", "failoverchoice": "FORCE"}, + ) + judge_command( + "CLUSTER FAILOVER takeover", + {"command": "CLUSTER FAILOVER", "failoverchoice": "takeover"}, + ) + judge_command( + "CLUSTER FAILOVER TAKEOVER", + {"command": "CLUSTER FAILOVER", "failoverchoice": "TAKEOVER"}, + ) + + +def test_command_cluster_forget(judge_command): + judge_command("cluster forget 1", {"command": "cluster forget", "node": "1"}) + judge_command( + "CLUSTER COUNT-FAILURE-REPORTS 1", + {"command": "CLUSTER COUNT-FAILURE-REPORTS", "node": "1"}, + ) + judge_command("cluster forget 1 2 3 4", None) + judge_command("cluster forget 1 a", None) + judge_command("cluster forget a", None) + judge_command("cluster forget a 2", None) + judge_command("cluster forget abc", None) + + +def test_command_cluster_getkeysinslot(judge_command): + judge_command( + "cluster getkeysinslot 1 1", + {"command": "cluster getkeysinslot", "slot": "1", "count": "1"}, + ) + judge_command( + "CLUSTER GETKEYSINSLOT 1 1", + {"command": "CLUSTER GETKEYSINSLOT", "slot": "1", "count": "1"}, + ) + judge_command( + "cluster getkeysinslot 1123 1121", + {"command": "cluster getkeysinslot", "slot": "1123", "count": "1121"}, + ) + judge_command("cluster getkeysinslot 1 2 3 4", None) + judge_command("cluster getkeysinslot 1 a", None) + judge_command("cluster getkeysinslot a", None) + judge_command("cluster getkeysinslot a 4", None) + judge_command("cluster getkeysinslot abc", None) + + +def test_command_cluster_info(judge_command): + judge_command("cluster info", {"command": "cluster info"}) + judge_command("CLUSTER INFO", {"command": "CLUSTER INFO"}) + judge_command("CLUSTER INFO 1", None) + judge_command("Acluster info", "invalid") + + +def test_command_cluster_keyslot(judge_command): + judge_command( + "cluster keyslot mykey", {"command": "cluster keyslot", "key": "mykey"} + ) + judge_command( + "cluster keyslot MYKEY", {"command": "cluster keyslot", "key": "MYKEY"} + ) + judge_command( + "CLUSTER KEYSLOT MYKEY", {"command": "CLUSTER KEYSLOT", "key": "MYKEY"} + ) + + +def test_command_cluster_meet(judge_command): + judge_command( + "cluster meet 192.168.0.1 12200", + {"command": "cluster meet", "ip": "192.168.0.1", "port": "12200"}, + ) + judge_command( + "CLUSTER MEET 192.168.0.1 12200", + {"command": "CLUSTER MEET", "ip": "192.168.0.1", "port": "12200"}, + ) + + +def test_command_cluster_nodes(judge_command): + judge_command("cluster nodes", {"command": "cluster nodes"}) + judge_command("CLUSTER NODES", {"command": "CLUSTER NODES"}) + + +def test_command_cluster_reset(judge_command): + judge_command( + "cluster reset hard", {"command": "cluster reset", "resetchoice": "hard"} + ) + judge_command( + "cluster reset soft", {"command": "cluster reset", "resetchoice": "soft"} + ) + judge_command( + "CLUSTER RESET HARD", {"command": "CLUSTER RESET", "resetchoice": "HARD"} + ) + judge_command( + "CLUSTER RESET soft", {"command": "CLUSTER RESET", "resetchoice": "soft"} + ) + judge_command( + "CLUSTER RESET SOFT", {"command": "CLUSTER RESET", "resetchoice": "SOFT"} + ) + judge_command("CLUSTER RESET SOFT1", None) + judge_command("CLUSTER RESET SAOFT", None) + + +def test_command_cluster_set_config_epoch(judge_command): + judge_command("cluster set-config-epoch 123123 ad", None) + judge_command( + "cluster set-config-epoch 0 ", + {"command": "cluster set-config-epoch", "epoch": "0"}, + ) + judge_command( + "cluster set-config-epoch 123123 ", + {"command": "cluster set-config-epoch", "epoch": "123123"}, + ) + + +def test_command_cluster_set_slot(judge_command): + judge_command( + "cluster setslot 123 importing 123123", + { + "command": "cluster setslot", + "slot": "123", + "slotsubcmd": "importing", + "node": "123123", + }, + ) + judge_command( + "cluster setslot 123 migrating 123123", + { + "command": "cluster setslot", + "slot": "123", + "slotsubcmd": "migrating", + "node": "123123", + }, + ) + judge_command( + "cluster setslot 123 node 123123", + { + "command": "cluster setslot", + "slot": "123", + "slotsubcmd": "node", + "node": "123123", + }, + ) + judge_command( + "cluster setslot 123 MIGRATING 123123", + { + "command": "cluster setslot", + "slot": "123", + "slotsubcmd": "MIGRATING", + "node": "123123", + }, + ) + judge_command( + "cluster setslot 123 stable", + {"command": "cluster setslot", "slot": "123", "slotsubcmd": "stable"}, + ) + judge_command( + "cluster setslot 123 STABLE", + {"command": "cluster setslot", "slot": "123", "slotsubcmd": "STABLE"}, + ) diff --git a/tests/unittests/command_parse/test_connection.py b/tests/unittests/command_parse/test_connection.py new file mode 100644 index 0000000..8952e53 --- /dev/null +++ b/tests/unittests/command_parse/test_connection.py @@ -0,0 +1,97 @@ +def test_auth(judge_command): + judge_command("auth 123", {"command": "auth", "password": "123"}) + + +def test_auth_redis6(judge_command): + from iredis.commands import command2syntax + from iredis.redis_grammar import get_command_grammar + + get_command_grammar.cache_clear() + + old = command2syntax["AUTH"] + command2syntax["AUTH"] = "command_usernamex_password" + judge_command( + "auth default 123", + {"command": "auth", "password": "123", "username": "default"}, + ) + judge_command("AUTH 123", {"command": "AUTH", "password": "123"}) + command2syntax["AUTH"] = old + + +def test_echo(judge_command): + judge_command("echo hello", {"command": "echo", "message": "hello"}) + + +def test_ping(judge_command): + judge_command("ping hello", {"command": "ping", "message": "hello"}) + judge_command("ping", {"command": "ping", "message": None}) + judge_command("ping hello world", None) + + +def test_select(judge_command): + for index in range(16): + judge_command(f"select {index}", {"command": "select", "index": str(index)}) + for index in range(16, 100): + judge_command(f"select {index}", None) + judge_command("select acb", None) + + +def test_swapdb(judge_command): + for index1 in range(16): + for index2 in range(16): + judge_command( + f"swapdb {index1} {index2}", + {"command": "swapdb", "index": [str(index1), str(index2)]}, + ) + judge_command("swapdb abc 1", None) + judge_command("swapdb 1", None) + + +def test_client_caching(judge_command): + judge_command("CLIENT CACHING YES", {"command": "CLIENT CACHING", "yes": "YES"}) + judge_command("CLIENT CACHING NO", {"command": "CLIENT CACHING", "yes": "NO"}) + judge_command("CLIENT CACHING", None) + judge_command("CLIENT CACHING abc", None) + + +def test_client_tracking(judge_command): + judge_command("CLIENT TRACKING on", {"command": "CLIENT TRACKING", "on_off": "on"}) + judge_command( + "CLIENT TRACKING ON REDIRECT 123", + { + "command": "CLIENT TRACKING", + "on_off": "ON", + "redirect_const": "REDIRECT", + "clientid": "123", + }, + ) + judge_command( + "CLIENT TRACKING ON PREFIX foo", + { + "command": "CLIENT TRACKING", + "on_off": "ON", + "prefix_const": "PREFIX", + "prefix": "foo", + }, + ) + judge_command( + "CLIENT TRACKING ON PREFIX foo", + { + "command": "CLIENT TRACKING", + "on_off": "ON", + "prefix_const": "PREFIX", + "prefix": "foo", + }, + ) + judge_command( + "CLIENT TRACKING ON PREFIX foo BCAST NOLOOP OPTIN", + { + "command": "CLIENT TRACKING", + "on_off": "ON", + "prefix_const": "PREFIX", + "prefix": "foo", + "bcast_const": "BCAST", + "noloop_const": "NOLOOP", + "optin_const": "OPTIN", + }, + ) diff --git a/tests/unittests/command_parse/test_generic_parse.py b/tests/unittests/command_parse/test_generic_parse.py new file mode 100644 index 0000000..f41d887 --- /dev/null +++ b/tests/unittests/command_parse/test_generic_parse.py @@ -0,0 +1,179 @@ +def test_del(judge_command): + judge_command("DEL abc", {"command": "DEL", "keys": "abc"}) + judge_command("DEL bc1", {"command": "DEL", "keys": "bc1"}) + judge_command("DEL 1", {"command": "DEL", "keys": "1"}) + judge_command('DEL "hello world"', {"command": "DEL", "keys": '"hello world"'}) + judge_command( + r'DEL "hell\"o world"', {"command": "DEL", "keys": r'"hell\"o world"'} + ) + judge_command("DEL abc def", {"command": "DEL", "keys": "abc def"}) + judge_command("DEL 1 2", {"command": "DEL", "keys": "1 2"}) + judge_command("DEL 'he \"llo'", {"command": "DEL", "keys": "'he \"llo'"}) + judge_command( + """DEL 'he "llo' "abc" 'def' """, + {"command": "DEL", "keys": "'he \"llo' \"abc\" 'def'"}, + ) + + +def test_exists(judge_command): + judge_command("EXISTS foo bar", {"command": "EXISTS", "keys": "foo bar"}) + judge_command("EXISTS foo", {"command": "EXISTS", "keys": "foo"}) + judge_command("EXISTS 1", {"command": "EXISTS", "keys": "1"}) + judge_command('EXISTS "foo bar"', {"command": "EXISTS", "keys": '"foo bar"'}) + judge_command(r'EXISTS "\""', {"command": "EXISTS", "keys": r'"\""'}) + + +def test_expire(judge_command): + judge_command("EXPIRE key 12", {"command": "EXPIRE", "key": "key", "second": "12"}) + judge_command("EXPIRE key a12", None) + judge_command("EXPIRE 12 12", {"command": "EXPIRE", "key": "12", "second": "12"}) + judge_command("EXPIRE 12", None) + + +def test_expireat(judge_command): + judge_command( + "EXPIRE key 1565787643", + {"command": "EXPIRE", "key": "key", "second": "1565787643"}, + ) + judge_command("EXPIRE key a12", None) + + +def test_keys(judge_command): + judge_command("KEYS *", {"command": "KEYS", "pattern": "*"}) + judge_command("KEYS *abc", {"command": "KEYS", "pattern": "*abc"}) + judge_command("keys abc*", {"command": "keys", "pattern": "abc*"}) + + +def test_move(judge_command): + judge_command("MOVE key 14", {"command": "MOVE", "key": "key", "index": "14"}) + + +def test_pexpire(judge_command): + judge_command( + "PEXPIRE key 12", {"command": "PEXPIRE", "key": "key", "millisecond": "12"} + ) + judge_command("PEXPIRE key a12", None) + judge_command( + "PEXPIRE 12 12", {"command": "PEXPIRE", "key": "12", "millisecond": "12"} + ) + judge_command("PEXPIRE 12", None) + + +def test_pexpireat(judge_command): + judge_command( + "PEXPIREAT key 1565787643", + {"command": "PEXPIREAT", "key": "key", "timestampms": "1565787643"}, + ) + judge_command("PEXPIREAT key a12", None) + + +def test_rename(judge_command): + judge_command( + "rename key newkey", {"command": "rename", "key": "key", "newkey": "newkey"} + ) + judge_command( + "rename 123 newkey", {"command": "rename", "key": "123", "newkey": "newkey"} + ) + judge_command("rename 123 ", None) + + +def test_scan(judge_command): + judge_command( + "SCAN 0 MATCH task* COUNT 15 TYPE string", + { + "command": "SCAN", + "cursor": "0", + "match": "MATCH", + "pattern": "task*", + "count_const": "COUNT", + "count": "15", + "type_const": "TYPE", + "type": "string", + }, + ) + judge_command("SCAN 0", {"command": "SCAN", "cursor": "0"}) + judge_command( + "SCAN 0 MATCH task*", + {"command": "SCAN", "cursor": "0", "match": "MATCH", "pattern": "task*"}, + ) + judge_command( + "SCAN 0 COUNT 15 TYPE string", + { + "command": "SCAN", + "cursor": "0", + "count_const": "COUNT", + "count": "15", + "type_const": "TYPE", + "type": "string", + }, + ) + + +def test_migrate(judge_command): + judge_command( + 'MIGRATE 192.168.1.34 6379 " " 0 5000 KEYS key1 key2 key3', + { + "command": "MIGRATE", + "host": "192.168.1.34", + "port": "6379", + "key": '" "', + "index": "0", + "timeout": "5000", + "const_keys": "KEYS", + "keys": "key1 key2 key3", + }, + ) + judge_command( + "MIGRATE 192.168.1.34 6379 foo 0 5000 auth password1 KEYS key1 key2 key3", + { + "command": "MIGRATE", + "host": "192.168.1.34", + "port": "6379", + "key": "foo", + "index": "0", + "timeout": "5000", + "const_keys": "KEYS", + "keys": "key1 key2 key3", + "auth": "auth", + "password": "password1", + }, + ) + judge_command( + "MIGRATE 192.168.1.34 6379 foo 0 5000 auth username1 password1 KEYS key1 key2 key3", + { + "command": "MIGRATE", + "host": "192.168.1.34", + "port": "6379", + "key": "foo", + "index": "0", + "timeout": "5000", + "const_keys": "KEYS", + "keys": "key1 key2 key3", + "auth": "auth", + "password": "password1", + "username": "username1", + }, + ) + + +def test_object(judge_command): + judge_command( + "object refcount mylist", + {"command": "object", "object": "refcount", "key": "mylist"}, + ) + + +def test_wait(judge_command): + judge_command("WAIT 3 100", {"command": "WAIT", "count": "3", "timeout": "100"}) + + +def test_restore(judge_command): + judge_command( + 'RESTORE mykey 0 "\n\x17\x17\x00\x00\x00\x12\x00\x00\x00\x03\x00\x00\xc0\x01\x00\x04\xc0\x02\x00\x04\xc0\x03\x00\xff\x04\x00u#<\xc0;.\xe9\xdd"', # noqa + { + "command": "RESTORE", + "key": "mykey", + "timeout": "0", + "value": '"\n\x17\x17\x00\x00\x00\x12\x00\x00\x00\x03\x00\x00\xc0\x01\x00\x04\xc0\x02\x00\x04\xc0\x03\x00\xff\x04\x00u#<\xc0;.\xe9\xdd"', # noqa + }, + ) diff --git a/tests/unittests/command_parse/test_geo.py b/tests/unittests/command_parse/test_geo.py new file mode 100644 index 0000000..8a8d4c7 --- /dev/null +++ b/tests/unittests/command_parse/test_geo.py @@ -0,0 +1,39 @@ +def test_geoadd(judge_command): + judge_command( + 'GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"', + { + "command": "GEOADD", + "key": "Sicily", + "longitude": "15.087269", + "latitude": "37.502669", + "member": '"Catania"', + }, + ) + + +def test_georadiusbymember(judge_command): + judge_command( + "GEORADIUSBYMEMBER Sicily Agrigento 100 km", + { + "command": "GEORADIUSBYMEMBER", + "key": "Sicily", + "member": "Agrigento", + "float": "100", + "distunit": "km", + }, + ) + + +def test_georadius(judge_command): + judge_command( + "GEORADIUS Sicily 15 37 200 km WITHDIST WITHCOORD ", + { + "command": "GEORADIUS", + "key": "Sicily", + "longitude": "15", + "latitude": "37", + "float": "200", + "distunit": "km", + "geochoice": "WITHCOORD", + }, + ) diff --git a/tests/unittests/command_parse/test_hash_parse.py b/tests/unittests/command_parse/test_hash_parse.py new file mode 100644 index 0000000..0c3aa04 --- /dev/null +++ b/tests/unittests/command_parse/test_hash_parse.py @@ -0,0 +1,45 @@ +def test_hdel(judge_command): + judge_command("HDEL foo bar", {"command": "HDEL", "key": "foo", "fields": "bar"}) + judge_command( + "HDEL foo bar hello world", + {"command": "HDEL", "key": "foo", "fields": "bar hello world"}, + ) + + +def test_hmset(judge_command): + judge_command( + "HMSET foo bar hello-world", + {"command": "HMSET", "key": "foo", "field": "bar", "value": "hello-world"}, + ) + judge_command( + "HMSET foo bar hello-world key2 value2", + {"command": "HMSET", "key": "foo", "field": "key2", "value": "value2"}, + ) + + +def test_hexists(judge_command): + judge_command( + "HEXISTS foo bar", {"command": "HEXISTS", "key": "foo", "field": "bar"} + ) + judge_command("HEXISTS foo bar hello-world", None) + + +def test_hincrby(judge_command): + judge_command( + "HINCRBY foo bar 12", + {"command": "HINCRBY", "key": "foo", "field": "bar", "delta": "12"}, + ) + + +def test_hincrbyfloat(judge_command): + judge_command( + "HINCRBYFLOAT foo bar 12.1", + {"command": "HINCRBYFLOAT", "key": "foo", "field": "bar", "float": "12.1"}, + ) + + +def test_hset(judge_command): + judge_command( + "HSET foo bar hello", + {"command": "HSET", "key": "foo", "field": "bar", "value": "hello"}, + ) diff --git a/tests/unittests/command_parse/test_hll_parse.py b/tests/unittests/command_parse/test_hll_parse.py new file mode 100644 index 0000000..b139eed --- /dev/null +++ b/tests/unittests/command_parse/test_hll_parse.py @@ -0,0 +1,5 @@ +def test_pfmerge(judge_command): + judge_command( + "PFMERGE hll3 hll1 hll2", + {"command": "PFMERGE", "newkey": "hll3", "keys": "hll1 hll2"}, + ) diff --git a/tests/unittests/command_parse/test_list_parse.py b/tests/unittests/command_parse/test_list_parse.py new file mode 100644 index 0000000..e60d7a2 --- /dev/null +++ b/tests/unittests/command_parse/test_list_parse.py @@ -0,0 +1,108 @@ +def test_rpush(judge_command): + judge_command( + "RPUSH list1 foo bar hello world", + {"command": "RPUSH", "key": "list1", "values": "foo bar hello world"}, + ) + judge_command( + "LPUSH list1 foo", {"command": "LPUSH", "key": "list1", "values": "foo"} + ) + + +def test_lindex(judge_command): + judge_command( + "LINDEX list1 10", {"command": "LINDEX", "key": "list1", "position": "10"} + ) + judge_command( + "LINDEX list1 -10", {"command": "LINDEX", "key": "list1", "position": "-10"} + ) + judge_command("LINDEX list1 1.1", None) + + +def test_lset(judge_command): + judge_command( + "LSET list1 10 newbie", + {"command": "LSET", "key": "list1", "position": "10", "value": "newbie"}, + ) + judge_command( + "LSET list1 -1 newbie", + {"command": "LSET", "key": "list1", "position": "-1", "value": "newbie"}, + ) + + +def test_brpoplpush(judge_command): + judge_command( + "BRPOPLPUSH list1 list2 10", + {"command": "BRPOPLPUSH", "key": "list1", "newkey": "list2", "timeout": "10"}, + ) + judge_command( + "BRPOPLPUSH list1 list2 0", + {"command": "BRPOPLPUSH", "key": "list1", "newkey": "list2", "timeout": "0"}, + ) + judge_command("BRPOPLPUSH list1 list2 -1", None) + + +def test_linsert(judge_command): + judge_command( + 'LINSERT mylist BEFORE "World" "There"', + { + "command": "LINSERT", + "key": "mylist", + "position_choice": "BEFORE", + "value": ['"World"', '"There"'], + }, + ) + judge_command( + 'LINSERT mylist after "World" "There"', + { + "command": "LINSERT", + "key": "mylist", + "position_choice": "after", + "value": ['"World"', '"There"'], + }, + ) + + +def test_lpos(judge_command): + judge_command("LPOS mylist c", {"command": "LPOS", "key": "mylist", "element": "c"}) + judge_command( + "LPOS mylist c RANK 2", + { + "command": "LPOS", + "key": "mylist", + "element": "c", + "rank_const": "RANK", + "rank": "2", + }, + ) + judge_command( + "LPOS mylist c RANK -1", + { + "command": "LPOS", + "key": "mylist", + "element": "c", + "rank_const": "RANK", + "rank": "-1", + }, + ) + judge_command( + "LPOS mylist c COUNT 2", + { + "command": "LPOS", + "key": "mylist", + "element": "c", + "count_const": "COUNT", + "count": "2", + }, + ) + judge_command( + "LPOS mylist c RANK -1 COUNT 2", + { + "command": "LPOS", + "key": "mylist", + "element": "c", + "count_const": "COUNT", + "count": "2", + "rank_const": "RANK", + "rank": "-1", + }, + ) diff --git a/tests/unittests/command_parse/test_pubsub.py b/tests/unittests/command_parse/test_pubsub.py new file mode 100644 index 0000000..c7a2b54 --- /dev/null +++ b/tests/unittests/command_parse/test_pubsub.py @@ -0,0 +1,15 @@ +def test_publish(judge_command): + judge_command( + "publish foo bar", {"command": "publish", "channel": "foo", "message": "bar"} + ) + + +def test_subscribe(judge_command): + judge_command("subscribe foo bar", {"command": "subscribe", "channel": "bar"}) + + +def test_pubsub(judge_command): + judge_command( + "PUBSUB NUMSUB foo bar", + {"command": "PUBSUB", "pubsubcmd": "NUMSUB", "channel": "bar"}, + ) diff --git a/tests/unittests/command_parse/test_script.py b/tests/unittests/command_parse/test_script.py new file mode 100644 index 0000000..3b984c3 --- /dev/null +++ b/tests/unittests/command_parse/test_script.py @@ -0,0 +1,22 @@ +def test_eval(judge_command): + judge_command( + 'eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second', + { + "command": "eval", + "double_lua": "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", + "any": "2 key1 key2 first second", + }, + ) + judge_command( + "eval 'return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}' 2 key1 key2 first second", + { + "command": "eval", + "single_lua": "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", + "any": "2 key1 key2 first second", + }, + ) + + +def test_scriptdebug(judge_command): + judge_command("SCRIPT DEBUG YES", {"command": "SCRIPT DEBUG", "scriptdebug": "YES"}) + judge_command("SCRIPT DEBUG no", {"command": "SCRIPT DEBUG", "scriptdebug": "no"}) diff --git a/tests/unittests/command_parse/test_server.py b/tests/unittests/command_parse/test_server.py new file mode 100644 index 0000000..5129e5f --- /dev/null +++ b/tests/unittests/command_parse/test_server.py @@ -0,0 +1,201 @@ +def test_client_setname(judge_command): + judge_command( + "CLIENT SETNAME foobar", {"command": "CLIENT SETNAME", "value": "foobar"} + ) + + +def test_client_unblock(judge_command): + judge_command( + "CLIENT UNBLOCK 33 TIMEOUT", + {"command": "CLIENT UNBLOCK", "clientid": "33", "error": "TIMEOUT"}, + ) + judge_command("CLIENT UNBLOCK 33", {"command": "CLIENT UNBLOCK", "clientid": "33"}) + + +def test_flushdb(judge_command): + judge_command("FLUSHDB async", {"command": "FLUSHDB", "async": "async"}) + judge_command("FLUSHDB", {"command": "FLUSHDB"}) + judge_command("FLUSHDB ASYNC", {"command": "FLUSHDB", "async": "ASYNC"}) + judge_command("FLUSHALL ASYNC", {"command": "FLUSHALL", "async": "ASYNC"}) + + +def test_client_list(judge_command): + judge_command("client list", {"command": "client list"}) + judge_command("client list TYPE REPLICA1", None) + judge_command( + "client list type master", + {"command": "client list", "type_const": "type", "conntype": "master"}, + ) + judge_command( + "client list TYPE REPLICA", + {"command": "client list", "type_const": "TYPE", "conntype": "REPLICA"}, + ) + + +def test_configset(judge_command): + judge_command( + "config set foo bar", + {"command": "config set", "parameter": "foo", "value": "bar"}, + ) + judge_command( + "config set requirepass ''", + {"command": "config set", "parameter": "requirepass", "value": "''"}, + ) + + +def test_shutdown(judge_command): + judge_command("shutdown save", {"command": "shutdown", "shutdown": "save"}) + judge_command("shutdown NOSAVE", {"command": "shutdown", "shutdown": "NOSAVE"}) + + +def test_clientpause(judge_command): + judge_command("client pause 3000", {"command": "client pause", "timeout": "3000"}) + + +def test_client_reply(judge_command): + judge_command("client reply on", {"command": "client reply", "switch": "on"}) + + +def test_client_kill(judge_command): + judge_command( + "CLIENT KILL addr 127.0.0.1:12345 type pubsub", + { + "command": "CLIENT KILL", + "addr": "addr", + "ip_port": "127.0.0.1:12345", + "type_const": "type", + "conntype": "pubsub", + }, + ) + judge_command( + "CLIENT KILL 127.0.0.1:12345 ", + {"command": "CLIENT KILL", "ip_port": "127.0.0.1:12345"}, + ) + judge_command( + "CLIENT KILL id 123455 type pubsub skipme no", + { + "command": "CLIENT KILL", + "const_id": "id", + "clientid": "123455", + "type_const": "type", + "conntype": "pubsub", + "skipme": "skipme", + "yes": "no", + }, + ) + + +def test_client_kill_username(judge_command): + """since redis-server 6.0""" + judge_command( + "client kill USER default", + {"command": "client kill", "const_user": "USER", "username": "default"}, + ) + + +def test_client_kill_unordered_arguments(judge_command): + judge_command( + "CLIENT KILL type pubsub addr 127.0.0.1:12345", + { + "command": "CLIENT KILL", + "addr": "addr", + "ip_port": "127.0.0.1:12345", + "type_const": "type", + "conntype": "pubsub", + }, + ) + + +def test_psync(judge_command): + judge_command( + "PSYNC abc 123", {"command": "PSYNC", "replicationid": "abc", "offset": "123"} + ) + judge_command("PSYNC", None) + + +def test_latency_graph(judge_command): + judge_command( + "latency graph command", {"command": "latency graph", "graphevent": "command"} + ) + judge_command( + "latency graph fork", {"command": "latency graph", "graphevent": "fork"} + ) + judge_command("latency graph", None) + + +def test_latency_reset(judge_command): + judge_command( + "latency reset command fork aof-fsync-always", + {"command": "latency reset", "graphevent": "aof-fsync-always"}, + ) + judge_command( + "latency reset fork", {"command": "latency reset", "graphevent": "fork"} + ) + judge_command("latency reset", {"command": "latency reset"}) + + +def test_lolwut(judge_command): + judge_command("lolwut", {"command": "lolwut"}) + # only works before redis 6 + judge_command("lolwut 5", {"command": "lolwut", "any": "5"}) + judge_command("lolwut 5 1", {"command": "lolwut", "any": "5 1"}) + # redis 6 + judge_command( + "lolwut VERSION 5 5", + {"command": "lolwut", "version": "VERSION", "version_num": "5", "any": "5"}, + ) + + +def test_info(judge_command): + judge_command("info cpu", {"command": "info", "section": "cpu"}) + judge_command("info", {"command": "info"}) + judge_command("info all", {"command": "info", "section": "all"}) + judge_command("info CPU", {"command": "info", "section": "CPU"}) + + +def test_bgsave(judge_command): + judge_command("bgsave", {"command": "bgsave"}) + judge_command("bgsave schedule", {"command": "bgsave", "schedule": "schedule"}) + judge_command("BGSAVE SCHEDULE", {"command": "BGSAVE", "schedule": "SCHEDULE"}) + + +def test_acl_cat(judge_command): + judge_command("acl cat", {"command": "acl cat"}) + judge_command("acl CAT", {"command": "acl CAT"}) + judge_command( + "ACL CAT scripting", {"command": "ACL CAT", "categoryname": "scripting"} + ) + judge_command("ACL CAT WATCH", {"command": "ACL CAT", "categoryname": "WATCH"}) + + +def test_acl_deluser(judge_command): + judge_command( + "acl deluser laixintao", {"command": "acl deluser", "username": "laixintao"} + ) + judge_command( + "acl deluser laixintao antirez", + {"command": "acl deluser", "username": "antirez"}, + ) + + +def test_acl_log(judge_command): + judge_command("acl log 2", {"command": "acl log", "count": "2"}) + judge_command("acl log reset", {"command": "acl log", "reset_const": "reset"}) + judge_command("acl log ", {"command": "acl log"}) + + +def test_acl_setuser(judge_command): + judge_command("ACL SETUSER alice", {"command": "ACL SETUSER", "username": "alice"}) + judge_command( + "ACL SETUSER alice on >p1pp0 ~cached:* +get", + {"command": "ACL SETUSER", "username": "alice", "rule": "+get"}, + ) + judge_command( + "ACL SETUSER alan allkeys +@string +@set -SADD >alanpassword", + {"command": "ACL SETUSER", "username": "alan", "rule": ">alanpassword"}, + ) + + +def test_acl_getuser(judge_command): + judge_command("acl getuser alan", {"command": "acl getuser", "username": "alan"}) + judge_command("acl getuser", None) diff --git a/tests/unittests/command_parse/test_set_parse.py b/tests/unittests/command_parse/test_set_parse.py new file mode 100644 index 0000000..d35a52b --- /dev/null +++ b/tests/unittests/command_parse/test_set_parse.py @@ -0,0 +1,40 @@ +def test_sadd(judge_command): + judge_command( + "SADD foo m1 m2 m3", {"command": "SADD", "key": "foo", "members": "m1 m2 m3"} + ) + judge_command("SADD foo m1", {"command": "SADD", "key": "foo", "members": "m1"}) + judge_command("SADD foo", None) + + +def test_sdiffstore(judge_command): + judge_command( + "SDIFFSTORE foo m1 m2 m3", + {"command": "SDIFFSTORE", "destination": "foo", "keys": "m1 m2 m3"}, + ) + judge_command( + "SDIFFSTORE foo m1", + {"command": "SDIFFSTORE", "destination": "foo", "keys": "m1"}, + ) + judge_command("SDIFFSTORE foo", None) + + +def test_is_member(judge_command): + judge_command("SISMEMBER foo m1 m2 m3", None) + judge_command( + "SISMEMBER foo m1", {"command": "SISMEMBER", "key": "foo", "member": "m1"} + ) + judge_command("SISMEMBER foo", None) + + +def test_smove(judge_command): + judge_command( + "SMOVE foo bar m2", + {"command": "SMOVE", "key": "foo", "newkey": "bar", "member": "m2"}, + ) + judge_command("SMOVE foo m1", None) + judge_command("SMOVE foo", None) + + +def test_spop(judge_command): + judge_command("SPOP set", {"command": "SPOP", "key": "set"}) + judge_command("SPOP set 3", {"command": "SPOP", "key": "set", "count": "3"}) diff --git a/tests/unittests/command_parse/test_sorted_set_parse.py b/tests/unittests/command_parse/test_sorted_set_parse.py new file mode 100644 index 0000000..a0af616 --- /dev/null +++ b/tests/unittests/command_parse/test_sorted_set_parse.py @@ -0,0 +1,172 @@ +import pytest + + +def test_zcount(judge_command): + judge_command( + "zcount foo -10 0", + {"command": "zcount", "key": "foo", "min": "-10", "max": "0"}, + ) + + +def test_bzpopmax(judge_command): + judge_command( + "bzpopmax set set2 set3 4", + {"command": "bzpopmax", "keys": "set set2 set3", "timeout": "4"}, + ) + judge_command( + "bzpopmin set 4", {"command": "bzpopmin", "keys": "set", "timeout": "4"} + ) + + +def test_zadd(judge_command): + judge_command( + "zadd t 100 qewqr 23 pp 11 oo", + { + "command": "zadd", + "key": "t", + "score": "11", # FIXME: only have last one + "member": "oo", + }, + ) + judge_command( + "zadd t incr 100 foo", + { + "command": "zadd", + "key": "t", + "incr": "incr", + "score": "100", # FIXME: only have last one + "member": "foo", + }, + ) + judge_command( + "zadd t NX CH incr 100 foo", + { + "command": "zadd", + "key": "t", + "condition": "NX", + "changed": "CH", + "incr": "incr", + "score": "100", # FIXME: only have last one + "member": "foo", + }, + ) + + +def test_zincrby(judge_command): + judge_command( + "zincrby t 10 foo", + {"command": "zincrby", "key": "t", "float": "10", "member": "foo"}, + ) + judge_command( + "zincrby t 2.3 foo", + {"command": "zincrby", "key": "t", "float": "2.3", "member": "foo"}, + ) + + +def test_zlexcount(judge_command): + judge_command( + "zlexcount a - +", + {"command": "zlexcount", "key": "a", "lexmin": "-", "lexmax": "+"}, + ) + judge_command( + "zlexcount a (aaaa [z", + {"command": "zlexcount", "key": "a", "lexmin": "(aaaa", "lexmax": "[z"}, + ) + judge_command( + "ZLEXCOUNT myset - [c", + {"command": "ZLEXCOUNT", "key": "myset", "lexmin": "-", "lexmax": "[c"}, + ) + judge_command( + "ZLEXCOUNT myset [aaa (g", + {"command": "ZLEXCOUNT", "key": "myset", "lexmin": "[aaa", "lexmax": "(g"}, + ) + + +def test_zrange(judge_command): + judge_command( + "zrange foo -1 10", + {"command": "zrange", "key": "foo", "start": "-1", "end": "10"}, + ) + judge_command( + "zrange foo 0 -1", + {"command": "zrange", "key": "foo", "start": "0", "end": "-1"}, + ) + judge_command( + "zrange foo 0 -1 withscores", + { + "command": "zrange", + "key": "foo", + "start": "0", + "end": "-1", + "withscores": "withscores", + }, + ) + + +@pytest.mark.xfail(reason="Not implemented yet") +def test_zinterstore(judge_command): + judge_command("ZINTERSTORE out 2 zset1 zset2 WEIGHTS 2 3", {}) + judge_command("ZINTERSTORE out 2 zset1 zset2 WEIGHTS -1 -2", {}) + judge_command("ZINTERSTORE out 2 zset1 zset2 WEIGHTS 0.2 0.3", {}) + + +def test_zrangebylex(judge_command): + judge_command( + "ZRANGEBYLEX myzset [aaa (g", + {"command": "ZRANGEBYLEX", "key": "myzset", "lexmin": "[aaa", "lexmax": "(g"}, + ) + judge_command( + "ZRANGEBYLEX myzset - (c", + {"command": "ZRANGEBYLEX", "key": "myzset", "lexmin": "-", "lexmax": "(c"}, + ) + judge_command( + "ZRANGEBYLEX myzset - (c limit 10 100", + { + "command": "ZRANGEBYLEX", + "key": "myzset", + "lexmin": "-", + "lexmax": "(c", + "limit": "limit", + "offset": "10", + "count": "100", + }, + ) + judge_command( + "ZRANGEBYLEX myzset - (c limit 10 -1", + { + "command": "ZRANGEBYLEX", + "key": "myzset", + "lexmin": "-", + "lexmax": "(c", + "limit": "limit", + "offset": "10", + "count": "-1", + }, + ) + + +def test_zrangebyscore(judge_command): + judge_command( + "ZRANGEBYSCORE myzset -inf +inf", + {"command": "ZRANGEBYSCORE", "key": "myzset", "min": "-inf", "max": "+inf"}, + ) + judge_command( + "ZRANGEBYSCORE myzset 1 2", + {"command": "ZRANGEBYSCORE", "key": "myzset", "min": "1", "max": "2"}, + ) + judge_command( + "ZRANGEBYSCORE myzset (1 (2", + {"command": "ZRANGEBYSCORE", "key": "myzset", "min": "(1", "max": "(2"}, + ) + judge_command( + "ZRANGEBYSCORE myzset -inf +inf LIMIT 10 100", + { + "command": "ZRANGEBYSCORE", + "key": "myzset", + "min": "-inf", + "max": "+inf", + "limit": "LIMIT", + "offset": "10", + "count": "100", + }, + ) diff --git a/tests/unittests/command_parse/test_stream.py b/tests/unittests/command_parse/test_stream.py new file mode 100644 index 0000000..5807aa2 --- /dev/null +++ b/tests/unittests/command_parse/test_stream.py @@ -0,0 +1,459 @@ +def test_xrange(judge_command): + judge_command( + "XRANGE somestream - +", + {"command": "XRANGE", "key": "somestream", "stream_id": ["-", "+"]}, + ) + judge_command( + "XRANGE somestream 1526985054069 1526985055069", + { + "command": "XRANGE", + "key": "somestream", + "stream_id": ["1526985054069", "1526985055069"], + }, + ) + judge_command( + "XRANGE somestream 1526985054069 1526985055069-10", + { + "command": "XRANGE", + "key": "somestream", + "stream_id": ["1526985054069", "1526985055069-10"], + }, + ) + judge_command( + "XRANGE somestream 1526985054069 1526985055069-10 count 10", + { + "command": "XRANGE", + "key": "somestream", + "stream_id": ["1526985054069", "1526985055069-10"], + "count_const": "count", + "count": "10", + }, + ) + + +def test_xgroup_create(judge_command): + judge_command( + "XGROUP CREATE mykey mygroup 123", + { + "command": "XGROUP", + "stream_create": "CREATE", + "key": "mykey", + "group": "mygroup", + "stream_id": "123", + }, + ) + judge_command( + "XGROUP CREATE mykey mygroup $", + { + "command": "XGROUP", + "stream_create": "CREATE", + "key": "mykey", + "group": "mygroup", + "stream_id": "$", + }, + ) + # short of a parameter + judge_command("XGROUP CREATE mykey mygroup", None) + judge_command("XGROUP CREATE mykey", None) + + +def test_xgroup_setid(judge_command): + judge_command( + "XGROUP SETID mykey mygroup 123", + { + "command": "XGROUP", + "stream_setid": "SETID", + "key": "mykey", + "group": "mygroup", + "stream_id": "123", + }, + ) + judge_command( + "XGROUP SETID mykey mygroup $", + { + "command": "XGROUP", + "stream_setid": "SETID", + "key": "mykey", + "group": "mygroup", + "stream_id": "$", + }, + ) + # two subcommand together shouldn't match + judge_command("XGROUP CREATE mykey mygroup 123 SETID mykey mygroup $", None) + + +def test_xgroup_destroy(judge_command): + judge_command( + "XGROUP destroy mykey mygroup", + { + "command": "XGROUP", + "stream_destroy": "destroy", + "key": "mykey", + "group": "mygroup", + }, + ) + judge_command("XGROUP destroy mykey", None) + judge_command("XGROUP DESTROY mykey mygroup $", None) + + +def test_xgroup_delconsumer(judge_command): + judge_command( + "XGROUP delconsumer mykey mygroup myconsumer", + { + "command": "XGROUP", + "stream_delconsumer": "delconsumer", + "key": "mykey", + "group": "mygroup", + "consumer": "myconsumer", + }, + ) + judge_command( + "XGROUP delconsumer mykey mygroup $", + { + "command": "XGROUP", + "stream_delconsumer": "delconsumer", + "key": "mykey", + "group": "mygroup", + "consumer": "$", + }, + ) + judge_command("XGROUP delconsumer mykey mygroup", None) + + +def test_xgroup_stream(judge_command): + judge_command( + "XACK mystream group1 123123", + { + "command": "XACK", + "key": "mystream", + "group": "group1", + "stream_id": "123123", + }, + ) + judge_command( + "XACK mystream group1 123123 111", + {"command": "XACK", "key": "mystream", "group": "group1", "stream_id": "111"}, + ) + + +def test_xinfo(judge_command): + judge_command( + "XINFO consumers mystream mygroup", + { + "command": "XINFO", + "stream_consumers": "consumers", + "key": "mystream", + "group": "mygroup", + }, + ) + judge_command( + "XINFO GROUPS mystream", + {"command": "XINFO", "stream_groups": "GROUPS", "key": "mystream"}, + ) + judge_command( + "XINFO STREAM mystream", + {"command": "XINFO", "stream": "STREAM", "key": "mystream"}, + ) + judge_command("XINFO HELP", {"command": "XINFO", "help": "HELP"}) + judge_command("XINFO consumers mystream mygroup GROUPS mystream", None) + judge_command("XINFO groups mystream mygroup", None) + + +def test_xinfo_with_full(judge_command): + judge_command( + "XINFO STREAM mystream FULL", + { + "command": "XINFO", + "stream": "STREAM", + "key": "mystream", + "full_const": "FULL", + }, + ) + judge_command( + "XINFO STREAM mystream FULL count 10", + { + "command": "XINFO", + "stream": "STREAM", + "key": "mystream", + "full_const": "FULL", + "count_const": "count", + "count": "10", + }, + ) + + +def test_xpending(judge_command): + judge_command( + "XPENDING mystream group55", + {"command": "XPENDING", "key": "mystream", "group": "group55"}, + ) + judge_command( + "XPENDING mystream group55 myconsumer", + { + "command": "XPENDING", + "key": "mystream", + "group": "group55", + "consumer": "myconsumer", + }, + ) + judge_command( + "XPENDING mystream group55 - + 10", + { + "command": "XPENDING", + "key": "mystream", + "group": "group55", + "stream_id": ["-", "+"], + "count": "10", + }, + ) + judge_command( + "XPENDING mystream group55 - + 10 myconsumer", + { + "command": "XPENDING", + "key": "mystream", + "group": "group55", + "stream_id": ["-", "+"], + "count": "10", + "consumer": "myconsumer", + }, + ) + judge_command("XPENDING mystream group55 - + ", None) + + +def test_xadd(judge_command): + judge_command( + "xadd mystream MAXLEN ~ 1000 * key value", + { + "command": "xadd", + "key": "mystream", + "maxlen": "MAXLEN", + "approximately": "~", + "count": "1000", + "sfield": "key", + "svalue": "value", + "stream_id": "*", + }, + ) + # test for MAXLEN option + judge_command( + "xadd mystream MAXLEN 1000 * key value", + { + "command": "xadd", + "key": "mystream", + "maxlen": "MAXLEN", + "count": "1000", + "sfield": "key", + "svalue": "value", + "stream_id": "*", + }, + ) + judge_command( + "xadd mystream * key value", + { + "command": "xadd", + "key": "mystream", + "sfield": "key", + "svalue": "value", + "stream_id": "*", + }, + ) + # spcify stream id + judge_command( + "xadd mystream 123-123 key value", + { + "command": "xadd", + "key": "mystream", + "sfield": "key", + "svalue": "value", + "stream_id": "123-123", + }, + ) + judge_command( + "xadd mystream 123-123 key value foo bar hello world", + { + "command": "xadd", + "key": "mystream", + "sfield": "hello", + "svalue": "world", + "stream_id": "123-123", + }, + ) + + +def test_xtrim(judge_command): + judge_command( + " XTRIM mystream MAXLEN 2", + {"command": "XTRIM", "key": "mystream", "maxlen": "MAXLEN", "count": "2"}, + ) + judge_command( + " XTRIM mystream MAXLEN ~ 2", + { + "command": "XTRIM", + "key": "mystream", + "maxlen": "MAXLEN", + "count": "2", + "approximately": "~", + }, + ) + judge_command(" XTRIM mystream", None) + + +def test_xdel(judge_command): + judge_command( + "XDEL mystream 1581165000000 1549611229000 1581060831000", + {"command": "XDEL", "key": "mystream", "stream_id": "1581060831000"}, + ) + judge_command( + "XDEL mystream 1581165000000", + {"command": "XDEL", "key": "mystream", "stream_id": "1581165000000"}, + ) + + +def test_xclaim(judge_command): + judge_command( + "XCLAIM mystream mygroup Alice 3600000 1526569498055-0", + { + "command": "XCLAIM", + "key": "mystream", + "group": "mygroup", + "consumer": "Alice", + "millisecond": "3600000", + "stream_id": "1526569498055-0", + }, + ) + judge_command( + "XCLAIM mystream mygroup Alice 3600000 1526569498055-0 123 456 789", + { + "command": "XCLAIM", + "key": "mystream", + "group": "mygroup", + "consumer": "Alice", + "millisecond": "3600000", + "stream_id": "789", + }, + ) + judge_command( + "XCLAIM mystream mygroup Alice 3600000 1526569498055-0 IDEL 300", + { + "command": "XCLAIM", + "key": "mystream", + "group": "mygroup", + "consumer": "Alice", + "millisecond": ["3600000", "300"], + "stream_id": "1526569498055-0", + "idel": "IDEL", + }, + ) + judge_command( + "XCLAIM mystream mygroup Alice 3600000 1526569498055-0 retrycount 7", + { + "command": "XCLAIM", + "key": "mystream", + "group": "mygroup", + "consumer": "Alice", + "millisecond": "3600000", + "stream_id": "1526569498055-0", + "retrycount": "retrycount", + "count": "7", + }, + ) + judge_command( + "XCLAIM mystream mygroup Alice 3600000 1526569498055-0 TIME 123456789", + { + "command": "XCLAIM", + "key": "mystream", + "group": "mygroup", + "consumer": "Alice", + "millisecond": "3600000", + "stream_id": "1526569498055-0", + "time": "TIME", + "timestamp": "123456789", + }, + ) + judge_command( + "XCLAIM mystream mygroup Alice 3600000 1526569498055-0 FORCE", + { + "command": "XCLAIM", + "key": "mystream", + "group": "mygroup", + "consumer": "Alice", + "millisecond": "3600000", + "stream_id": "1526569498055-0", + "force": "FORCE", + }, + ) + judge_command( + "XCLAIM mystream mygroup Alice 3600000 1526569498055-0 JUSTID", + { + "command": "XCLAIM", + "key": "mystream", + "group": "mygroup", + "consumer": "Alice", + "millisecond": "3600000", + "stream_id": "1526569498055-0", + "justid": "JUSTID", + }, + ) + + +def test_xread(judge_command): + judge_command( + "XREAD COUNT 2 STREAMS mystream writers 0-0 0-0", + { + "command": "XREAD", + "count_const": "COUNT", + "count": "2", + "streams": "STREAMS", + # FIXME current grammar can't support multiple tokens + # so the ids will be recongized to keys. + "keys": "mystream writers 0-0", + "stream_id": "0-0", + }, + ) + judge_command( + "XREAD COUNT 2 BLOCK 1000 STREAMS mystream writers 0-0 0-0", + { + "command": "XREAD", + "count_const": "COUNT", + "count": "2", + "streams": "STREAMS", + "keys": "mystream writers 0-0", + "block": "BLOCK", + "millisecond": "1000", + "stream_id": "0-0", + }, + ) + + +def test_xreadgroup(judge_command): + judge_command( + "XREADGROUP GROUP mygroup1 Bob COUNT 1 BLOCK 100 NOACK STREAMS key1 1 key2 2", + { + "command": "XREADGROUP", + "stream_group": "GROUP", + "group": "mygroup1", + "consumer": "Bob", + "count_const": "COUNT", + "count": "1", + "block": "BLOCK", + "millisecond": "100", + "noack": "NOACK", + "streams": "STREAMS", + "keys": "key1 1 key2", + "stream_id": "2", + }, + ) + judge_command( + "XREADGROUP GROUP mygroup1 Bob STREAMS key1 1 key2 2", + { + "command": "XREADGROUP", + "stream_group": "GROUP", + "group": "mygroup1", + "consumer": "Bob", + "streams": "STREAMS", + "keys": "key1 1 key2", + "stream_id": "2", + }, + ) + + judge_command("XREADGROUP GROUP group consumer", None) diff --git a/tests/unittests/command_parse/test_string_parse.py b/tests/unittests/command_parse/test_string_parse.py new file mode 100644 index 0000000..df56b25 --- /dev/null +++ b/tests/unittests/command_parse/test_string_parse.py @@ -0,0 +1,345 @@ +def test_set(judge_command): + judge_command("SET abc bar", {"command": "SET", "key": "abc", "value": "bar"}) + judge_command( + "SET abc bar EX 10", + { + "command": "SET", + "key": "abc", + "value": "bar", + "expiration": "EX", + "millisecond": "10", + }, + ) + judge_command( + "SET abc bar px 10000", + { + "command": "SET", + "key": "abc", + "value": "bar", + "expiration": "px", + "millisecond": "10000", + }, + ) + judge_command( + "SET abc bar px 10000 nx", + { + "command": "SET", + "key": "abc", + "value": "bar", + "expiration": "px", + "millisecond": "10000", + "condition": "nx", + }, + ) + judge_command( + "SET abc bar px 10000 XX", + { + "command": "SET", + "key": "abc", + "value": "bar", + "expiration": "px", + "millisecond": "10000", + "condition": "XX", + }, + ) + judge_command( + "SET abc bar XX px 10000", + { + "command": "SET", + "key": "abc", + "value": "bar", + "expiration": "px", + "millisecond": "10000", + "condition": "XX", + }, + ) + judge_command( + "SET abc bar XX", + {"command": "SET", "key": "abc", "value": "bar", "condition": "XX"}, + ) + # keepttl + judge_command( + "SET abc bar XX keepttl", + { + "command": "SET", + "key": "abc", + "value": "bar", + "condition": "XX", + "keepttl": "keepttl", + }, + ) + judge_command( + "SET abc bar keepttl XX", + { + "command": "SET", + "key": "abc", + "value": "bar", + "condition": "XX", + "keepttl": "keepttl", + }, + ) + judge_command( + "SET abc bar XX px 10000 KEEPTTL", + { + "command": "SET", + "key": "abc", + "value": "bar", + "expiration": "px", + "millisecond": "10000", + "condition": "XX", + "keepttl": "KEEPTTL", + }, + ) + + +def test_append(judge_command): + judge_command("append foo bar", {"command": "append", "key": "foo", "value": "bar"}) + judge_command( + "APPEND foo 'bar'", {"command": "APPEND", "key": "foo", "value": "'bar'"} + ) + judge_command("APPEND foo", None) + + +def test_bitcount(judge_command): + judge_command("bitcount foo", {"command": "bitcount", "key": "foo"}) + judge_command( + "bitcount foo 1 5", + {"command": "bitcount", "key": "foo", "start": "1", "end": "5"}, + ) + judge_command( + "bitcount foo 1 -5", + {"command": "bitcount", "key": "foo", "start": "1", "end": "-5"}, + ) + judge_command( + "bitcount foo -2 -1", + {"command": "bitcount", "key": "foo", "start": "-2", "end": "-1"}, + ) + judge_command("bitcount foo -2", None) + + +def test_getrange(judge_command): + judge_command("getrange foo", None) + judge_command( + "getrange foo 1 5", + {"command": "getrange", "key": "foo", "start": "1", "end": "5"}, + ) + judge_command( + "getrange foo 1 -5", + {"command": "getrange", "key": "foo", "start": "1", "end": "-5"}, + ) + judge_command( + "getrange foo -2 -1", + {"command": "getrange", "key": "foo", "start": "-2", "end": "-1"}, + ) + judge_command("getrange foo -2", None) + + +def test_get_set(judge_command): + judge_command("GETSET abc bar", {"command": "GETSET", "key": "abc", "value": "bar"}) + + +def test_incr(judge_command): + judge_command("INCR foo", {"command": "INCR", "key": "foo"}) + judge_command("INCR", None) + judge_command("INCR foo 1", None) + + +def test_incr_by(judge_command): + judge_command("INCRBY foo", None) + judge_command("INCRBY", None) + judge_command("INCRBY foo 1", {"command": "INCRBY", "key": "foo", "delta": "1"}) + judge_command("INCRBY foo 200", {"command": "INCRBY", "key": "foo", "delta": "200"}) + judge_command("INCRBY foo -21", {"command": "INCRBY", "key": "foo", "delta": "-21"}) + + +def test_decr(judge_command): + judge_command("DECR foo", {"command": "DECR", "key": "foo"}) + judge_command("DECR", None) + judge_command("DECR foo 1", None) + + +def test_decr_by(judge_command): + judge_command("DECRBY foo", None) + judge_command("DECRBY", None) + judge_command("DECRBY foo 1", {"command": "DECRBY", "key": "foo", "delta": "1"}) + judge_command("DECRBY foo 200", {"command": "DECRBY", "key": "foo", "delta": "200"}) + judge_command("DECRBY foo -21", {"command": "DECRBY", "key": "foo", "delta": "-21"}) + + +def test_command_set_range(judge_command): + judge_command( + "SETRANGE foo 10 bar", + {"command": "SETRANGE", "key": "foo", "offset": "10", "value": "bar"}, + ) + judge_command("SETRANGE foo bar", None) + judge_command( + "SETRANGE Redis 10 'hello world'", + { + "command": "SETRANGE", + "key": "Redis", + "offset": "10", + "value": "'hello world'", + }, + ) + + +def test_command_set_ex(judge_command): + judge_command( + "SETEX key 10 value", + {"command": "SETEX", "key": "key", "second": "10", "value": "value"}, + ) + judge_command("SETEX foo 10", None) + judge_command( + "setex Redis 10 'hello world'", + {"command": "setex", "key": "Redis", "second": "10", "value": "'hello world'"}, + ) + + +def test_command_setbit(judge_command): + judge_command( + "SETBIT key 10 0", + {"command": "SETBIT", "key": "key", "offset": "10", "bit": "0"}, + ) + judge_command( + "SETBIT foo 10 1", + {"command": "SETBIT", "key": "foo", "offset": "10", "bit": "1"}, + ) + judge_command("SETBIT foo 10 10", None) + judge_command("SETBIT foo 10 abc", None) + judge_command("SETBIT foo 10", None) + judge_command("SETBIT foo", None) + + +def test_command_getbit(judge_command): + judge_command("GETBIT key 10", {"command": "GETBIT", "key": "key", "offset": "10"}) + judge_command("GETBIT foo 0", {"command": "GETBIT", "key": "foo", "offset": "0"}) + judge_command("GETBIT foo -1", None) + judge_command("SETBIT foo abc", None) + judge_command("SETBIT foo", None) + + +def test_command_incrbyfloat(judge_command): + judge_command("INCRBYFLOAT key", None) + judge_command( + "INCRBYFLOAT key 1.1", {"command": "INCRBYFLOAT", "key": "key", "float": "1.1"} + ) + judge_command( + "INCRBYFLOAT key .1", {"command": "INCRBYFLOAT", "key": "key", "float": ".1"} + ) + judge_command( + "INCRBYFLOAT key 1.", {"command": "INCRBYFLOAT", "key": "key", "float": "1."} + ) + judge_command( + "INCRBYFLOAT key 5.0e3", + {"command": "INCRBYFLOAT", "key": "key", "float": "5.0e3"}, + ) + judge_command( + "INCRBYFLOAT key -5.0e3", + {"command": "INCRBYFLOAT", "key": "key", "float": "-5.0e3"}, + ) + + +def test_command_mget(judge_command): + judge_command("mget foo bar", {"command": "mget", "keys": "foo bar"}) + + +def test_mset(judge_command): + judge_command("mset foo bar", {"command": "mset", "key": "foo", "value": "bar"}) + judge_command( + "mset foo bar hello world", + {"command": "mset", "key": "hello", "value": "world"}, + ) + + +def test_psetex(judge_command): + judge_command( + "psetex foo 1000 bar", + {"command": "psetex", "key": "foo", "value": "bar", "millisecond": "1000"}, + ) + judge_command("psetex foo bar", None) + + +def test_bitop(judge_command): + judge_command( + "BITOP AND dest key1 key2", + {"command": "BITOP", "operation": "AND", "key": "dest", "keys": "key1 key2"}, + ) + judge_command( + "BITOP AND dest key1", + {"command": "BITOP", "operation": "AND", "key": "dest", "keys": "key1"}, + ) + judge_command("BITOP AND dest", None) + + +def test_bitpos(judge_command): + judge_command( + "BITPOS mykey 1 3 5", + {"command": "BITPOS", "key": "mykey", "bit": "1", "start": "3", "end": "5"}, + ) + judge_command("BITPOS mykey 1", {"command": "BITPOS", "key": "mykey", "bit": "1"}) + judge_command( + "BITPOS mykey 1 3", + {"command": "BITPOS", "key": "mykey", "bit": "1", "start": "3"}, + ) + + +def test_bitfield(judge_command): + judge_command( + "BITFIELD mykey INCRBY i5 100 1 GET u4 0", + { + "command": "BITFIELD", + "key": "mykey", + "incrby": "INCRBY", + "inttype": ["i5", "u4"], + "offset": ["100", "0"], + "value": "1", + "get": "GET", + }, + ) + judge_command( + "BITFIELD mystring SET i8 #0 100", + { + "command": "BITFIELD", + "key": "mystring", + "set": "SET", + "inttype": "i8", + "offset": "#0", + "value": "100", + }, + ) + judge_command( + "BITFIELD mykey incrby u2 100 1 OVERFLOW SAT incrby u2 102 1", + { + "command": "BITFIELD", + "key": "mykey", + "incrby": "incrby", + "inttype": "u2", + "offset": "102", + "value": "1", + "overflow": "OVERFLOW", + "overflow_option": "SAT", + }, + ) + + +def test_stralgo(judge_command): + judge_command( + "STRALGO LCS STRINGS ohmytext mynewtext", + { + "command": "STRALGO", + "str_algo": "LCS", + "strings_const": "STRINGS", + "values": "ohmytext mynewtext", + }, + ) + + # Due to redis' command design, this can't be fix in any ways. + judge_command( + "STRALGO LCS STRINGS ohmytext mynewtext LEN", + { + "command": "STRALGO", + "str_algo": "LCS", + "strings_const": "STRINGS", + "values": "ohmytext mynewtext LEN", + }, + ) diff --git a/tests/unittests/test_client.py b/tests/unittests/test_client.py new file mode 100644 index 0000000..e78da17 --- /dev/null +++ b/tests/unittests/test_client.py @@ -0,0 +1,504 @@ +import re +import pytest +import redis +from unittest.mock import MagicMock, patch +from textwrap import dedent + +from prompt_toolkit.formatted_text import FormattedText + +from iredis.client import Client +from iredis.config import config, load_config_files +from iredis.completers import IRedisCompleter +from iredis.entry import Rainbow, prompt_message +from iredis.exceptions import NotSupport + + +@pytest.fixture +def completer(): + return IRedisCompleter() + + +@pytest.mark.parametrize( + "_input, command_name, expect_args", + [ + ("keys *", "keys", ["*"]), + ("DEL abc foo bar", "DEL", ["abc", "foo", "bar"]), + ("cluster info", "cluster info", []), + ("CLUSTER failover FORCE", "CLUSTER failover", ["FORCE"]), + ], +) +def test_send_command(_input, command_name, expect_args): + client = Client("127.0.0.1", "6379", None) + client.execute = MagicMock() + next(client.send_command(_input, None)) + args, kwargs = client.execute.call_args + assert args == (command_name, *expect_args) + + +def test_client_not_support_hello_command(iredis_client): + with pytest.raises(NotSupport): + iredis_client.pre_hook("hello 3", "hello", "3", None) + + +def test_patch_completer(): + client = Client("127.0.0.1", "6379", None) + completer = IRedisCompleter() + client.pre_hook( + "MGET foo bar hello world", "MGET", "foo bar hello world", completer + ) + assert completer.key_completer.words == ["world", "hello", "bar", "foo"] + assert completer.key_completer.words == ["world", "hello", "bar", "foo"] + + client.pre_hook("GET bar", "GET", "bar", completer) + assert completer.key_completer.words == ["bar", "world", "hello", "foo"] + + +def test_get_server_verison_after_client(config): + Client("127.0.0.1", "6379", None) + assert re.match(r"\d+\..*", config.version) + + config.version = "Unknown" + config.no_info = True + Client("127.0.0.1", "6379", None) + assert config.version == "Unknown" + + +def test_do_help(config): + client = Client("127.0.0.1", "6379", None) + config.version = "5.0.0" + resp = client.do_help("SET") + assert resp[10] == ("", "1.0.0 (Avaiable on your redis-server: 5.0.0)") + config.version = "2.0.0" + resp = client.do_help("cluster", "addslots") + assert resp[10] == ("", "3.0.0 (Not avaiable on your redis-server: 2.0.0)") + + +def test_rainbow_iterator(): + "test color infinite iterator" + original_color = Rainbow.color + Rainbow.color = list(range(0, 3)) + assert list(zip(range(10), Rainbow())) == [ + (0, 0), + (1, 1), + (2, 2), + (3, 1), + (4, 0), + (5, 1), + (6, 2), + (7, 1), + (8, 0), + (9, 1), + ] + Rainbow.color = original_color + + +def test_prompt_message(iredis_client, config): + config.rainbow = False + assert prompt_message(iredis_client) == "127.0.0.1:6379[15]> " + + config.rainbow = True + assert prompt_message(iredis_client)[:3] == [ + ("#cc2244", "1"), + ("#bb4444", "2"), + ("#996644", "7"), + ] + + +def test_on_connection_error_retry(iredis_client, config): + config.retry_times = 1 + mock_connection = MagicMock() + mock_connection.read_response.side_effect = [ + redis.exceptions.ConnectionError( + "Error 61 connecting to 127.0.0.1:7788. Connection refused." + ), + "hello", + ] + original_connection = iredis_client.connection + iredis_client.connection = mock_connection + value = iredis_client.execute("None", "GET", ["foo"]) + assert value == "hello" # be rendered + + mock_connection.disconnect.assert_called_once() + mock_connection.connect.assert_called_once() + + iredis_client.connection = original_connection + + +def test_on_connection_error_retry_without_retrytimes(iredis_client, config): + config.retry_times = 0 + mock_connection = MagicMock() + mock_connection.read_response.side_effect = [ + redis.exceptions.ConnectionError( + "Error 61 connecting to 127.0.0.1:7788. Connection refused." + ), + "hello", + ] + iredis_client.connection = mock_connection + with pytest.raises(redis.exceptions.ConnectionError): + iredis_client.execute("None", "GET", ["foo"]) + + mock_connection.disconnect.assert_not_called() + mock_connection.connect.assert_not_called() + + +def test_socket_keepalive(config): + config.socket_keepalive = True + from iredis.client import Client + + newclient = Client("127.0.0.1", "6379", 0) + assert newclient.connection.socket_keepalive + + # keepalive off + config.socket_keepalive = False + + newclient = Client("127.0.0.1", "6379", 0) + assert not newclient.connection.socket_keepalive + + +def test_not_retry_on_authentication_error(iredis_client, config): + config.retry_times = 2 + mock_connection = MagicMock() + mock_connection.read_response.side_effect = [ + redis.exceptions.AuthenticationError("Authentication required."), + "hello", + ] + iredis_client.connection = mock_connection + with pytest.raises(redis.exceptions.ConnectionError): + iredis_client.execute("None", "GET", ["foo"]) + + +@pytest.mark.skipif("int(os.environ['REDIS_VERSION']) < 6") +def test_auto_select_db_and_auth_for_reconnect_only_6(iredis_client, config): + config.retry_times = 2 + config.raw = True + next(iredis_client.send_command("select 2")) + assert iredis_client.connection.db == 2 + + resp = next(iredis_client.send_command("auth 123")) + + assert ( + b"ERROR AUTH called without any " + b"password configured for the default user. " + b"Are you sure your configuration is correct?" in resp + ) + assert iredis_client.connection.password is None + + next(iredis_client.send_command("config set requirepass 'abc'")) + next(iredis_client.send_command("auth abc")) + assert iredis_client.connection.password == "abc" + assert ( + iredis_client.execute("ACL SETUSER", "default", "on", "nopass", "~*", "+@all") + == b"OK" + ) + + +@pytest.mark.skipif("int(os.environ['REDIS_VERSION']) > 5") +def test_auto_select_db_and_auth_for_reconnect_only_5(iredis_client, config): + config.retry_times = 2 + config.raw = True + next(iredis_client.send_command("select 2")) + assert iredis_client.connection.db == 2 + + resp = next(iredis_client.send_command("auth 123")) + + assert b"Client sent AUTH, but no password is set" in resp + assert iredis_client.connection.password is None + + next(iredis_client.send_command("config set requirepass 'abc'")) + next(iredis_client.send_command("auth abc")) + assert iredis_client.connection.password == "abc" + next(iredis_client.send_command("config set requirepass ''")) + + +def test_split_shell_command(iredis_client, completer): + assert iredis_client.split_command_and_pipeline(" get json | rg . ", completer) == ( + " get json ", + "rg . ", + ) + + assert iredis_client.split_command_and_pipeline( + """ get "json | \\" hello" | rg . """, completer + ) == (""" get "json | \\" hello" """, "rg . ") + + +def test_running_with_pipeline(clean_redis, iredis_client, capfd, completer): + config.shell = True + clean_redis.set("foo", "hello \n world") + with pytest.raises(StopIteration): + next(iredis_client.send_command("get foo | grep w", completer)) + out, err = capfd.readouterr() + assert out == " world\n" + + +def test_running_with_multiple_pipeline(clean_redis, iredis_client, capfd, completer): + config.shell = True + clean_redis.set("foo", "hello world\nhello iredis") + with pytest.raises(StopIteration): + next( + iredis_client.send_command("get foo | grep hello | grep iredis", completer) + ) + out, err = capfd.readouterr() + assert out == "hello iredis\n" + + +def test_can_not_connect_on_startup(capfd): + with pytest.raises(SystemExit): + Client("localhost", "16111", 15) + out, err = capfd.readouterr() + assert "connecting to localhost:16111." in err + + +def test_peek_key_not_exist(iredis_client, clean_redis, config): + config.raw = False + peek_result = list(iredis_client.do_peek("non-exist-key")) + assert peek_result == ["non-exist-key doesn't exist."] + + +def test_peek_string(iredis_client, clean_redis): + clean_redis.set("foo", "bar") + peek_result = list(iredis_client.do_peek("foo")) + + assert peek_result == [ + FormattedText( + [ + ("class:dockey", "key: "), + ("", "string (embstr) mem: 50 bytes, ttl: -1"), + ("", "\n"), + ("class:dockey", "strlen: "), + ("", "3"), + ("", "\n"), + ("class:dockey", "value: "), + ("", '"bar"'), + ] + ) + ] + + +def test_peek_list_fetch_all(iredis_client, clean_redis): + clean_redis.lpush("mylist", *[f"hello-{index}" for index in range(5)]) + peek_result = list(iredis_client.do_peek("mylist")) + + assert peek_result == [ + FormattedText( + [ + ("class:dockey", "key: "), + ("", "list (quicklist) mem: 176 bytes, ttl: -1"), + ("", "\n"), + ("class:dockey", "llen: "), + ("", "5"), + ("", "\n"), + ("class:dockey", "elements: "), + ("", "\n"), + ("", "1)"), + ("", " "), + ("class:string", '"hello-4"'), + ("", "\n"), + ("", "2)"), + ("", " "), + ("class:string", '"hello-3"'), + ("", "\n"), + ("", "3)"), + ("", " "), + ("class:string", '"hello-2"'), + ("", "\n"), + ("", "4)"), + ("", " "), + ("class:string", '"hello-1"'), + ("", "\n"), + ("", "5)"), + ("", " "), + ("class:string", '"hello-0"'), + ] + ) + ] + + +def test_peek_list_fetch_part(iredis_client, clean_redis): + clean_redis.lpush("mylist", *[f"hello-{index}" for index in range(40)]) + peek_result = list(iredis_client.do_peek("mylist")) + assert len(peek_result[0]) == 91 + + +def test_peek_set_fetch_all(iredis_client, clean_redis): + clean_redis.sadd("myset", *[f"hello-{index}" for index in range(5)]) + peek_result = list(iredis_client.do_peek("myset")) + assert len(peek_result[0]) == 27 + + +def test_peek_set_fetch_part(iredis_client, clean_redis): + clean_redis.sadd("myset", *[f"hello-{index}" for index in range(40)]) + peek_result = list(iredis_client.do_peek("myset")) + + assert peek_result[0][0] == ("class:dockey", "key: ") + assert peek_result[0][1][1].startswith("set (hashtable) mem: 2") + + +def test_peek_zset_fetch_all(iredis_client, clean_redis): + clean_redis.zadd( + "myzset", dict(zip([f"hello-{index}" for index in range(3)], range(3))) + ) + peek_result = list(iredis_client.do_peek("myzset")) + assert peek_result[0][0:9] == FormattedText( + [ + ("class:dockey", "key: "), + ("", "zset (ziplist) mem: 92 bytes, ttl: -1"), + ("", "\n"), + ("class:dockey", "zcount: "), + ("", "3"), + ("", "\n"), + ("class:dockey", "members: "), + ("", "\n"), + ("", "1)"), + ] + ) + + +def test_peek_zset_fetch_part(iredis_client, clean_redis): + clean_redis.zadd( + "myzset", dict(zip([f"hello-{index}" for index in range(40)], range(40))) + ) + peek_result = list(iredis_client.do_peek("myzset")) + assert peek_result[0][0:8] == FormattedText( + [ + ("class:dockey", "key: "), + ("", "zset (ziplist) mem: 556 bytes, ttl: -1"), + ("", "\n"), + ("class:dockey", "zcount: "), + ("", "40"), + ("", "\n"), + ("class:dockey", "members (first 40): "), + ("", "\n"), + ] + ) + + +def test_peek_hash_fetch_all(iredis_client, clean_redis): + for key, value in zip( + [f"hello-{index}" for index in range(3)], [f"hi-{index}" for index in range(3)] + ): + clean_redis.hset("myhash", key, value) + peek_result = list(iredis_client.do_peek("myhash")) + assert len(peek_result[0]) == 28 + + +def test_peek_hash_fetch_part(iredis_client, clean_redis): + for key, value in zip( + [f"hello-{index}" for index in range(100)], + [f"hi-{index}" for index in range(100)], + ): + clean_redis.hset("myhash", key, value) + peek_result = list(iredis_client.do_peek("myhash")) + assert len(peek_result[0]) == 707 + + +def test_peek_stream(iredis_client, clean_redis): + clean_redis.xadd("mystream", {"foo": "bar", "hello": "world"}) + peek_result = list(iredis_client.do_peek("mystream")) + + assert peek_result[0][0:18] == FormattedText( + [ + ("class:dockey", "key: "), + ("", "stream (unknown) mem: 601 bytes, ttl: -1"), + ("", "\n"), + ("class:dockey", "XINFO: "), + ("", "\n"), + ("", " 1)"), + ("", " "), + ("class:string", '"length"'), + ("", "\n"), + ("", " 2)"), + ("", " "), + ("class:string", '"1"'), + ("", "\n"), + ("", " 3)"), + ("", " "), + ("class:string", '"radix-tree-keys"'), + ("", "\n"), + ("", " 4)"), + ] + ) + + +def test_mem_not_called_before_redis_4(config, iredis_client, clean_redis): + config.version = "3.2.9" + + def wrapper(func): + def execute(command_name, *args): + print(command_name) + if command_name.upper() == "MEMORY USAGE": + raise Exception("MEMORY USAGE not supported!") + return func(command_name, *args) + + return execute + + iredis_client.execute = wrapper(iredis_client.execute) + clean_redis.set("foo", "bar") + result = list(iredis_client.do_peek("foo")) + assert result[0][1] == ("", "string (embstr), ttl: -1") + + +def test_mem_not_called_when_cant_get_server_version( + config, iredis_client, clean_redis +): + config.version = None + + def wrapper(func): + def execute(command_name, *args): + print(command_name) + if command_name.upper() == "MEMORY USAGE": + raise Exception("MEMORY USAGE not supported!") + return func(command_name, *args) + + return execute + + iredis_client.execute = wrapper(iredis_client.execute) + clean_redis.set("foo", "bar") + result = list(iredis_client.do_peek("foo")) + assert result[0][1] == ("", "string (embstr), ttl: -1") + + +def test_reissue_command_on_redis_cluster(iredis_client, clean_redis): + mock_response = iredis_client.connection = MagicMock() + mock_response.read_response.side_effect = redis.exceptions.ResponseError( + "MOVED 12182 127.0.0.1:7002" + ) + iredis_client.reissue_with_redirect = MagicMock() + iredis_client.execute("set", "foo", "bar") + assert iredis_client.reissue_with_redirect.call_args == ( + ( + "MOVED 12182 127.0.0.1:7002", + "set", + "foo", + "bar", + ), + ) + + +def test_reissue_command_on_redis_cluster_with_password_in_dsn( + iredis_client, clean_redis +): + config_content = dedent( + """ + [main] + log_location = /tmp/iredis1.log + no_info=True + [alias_dsn] + cluster-7003=redis://foo:bar@127.0.0.1:7003 + """ + ) + with open("/tmp/iredisrc", "w+") as etc_config: + etc_config.write(config_content) + + config_obj = load_config_files("/tmp/iredisrc") + config.alias_dsn = config_obj["alias_dsn"] + + mock_execute_by_connection = iredis_client.execute_by_connection = MagicMock() + with patch("redis.connection.Connection.connect"): + iredis_client.reissue_with_redirect( + "MOVED 12182 127.0.0.1:7003", "set", "foo", "bar" + ) + + call_args = mock_execute_by_connection.call_args[0] + print(call_args) + assert list(call_args[1:]) == ["set", "foo", "bar"] + assert call_args[0].password == "bar" diff --git a/tests/unittests/test_completers.py b/tests/unittests/test_completers.py new file mode 100644 index 0000000..d0344fd --- /dev/null +++ b/tests/unittests/test_completers.py @@ -0,0 +1,356 @@ +from unittest.mock import MagicMock, patch + +import pendulum +from prompt_toolkit.formatted_text import FormattedText +from prompt_toolkit.completion import Completion + +from iredis.completers import MostRecentlyUsedFirstWordCompleter +from iredis.completers import IRedisCompleter, TimestampCompleter, IntegerTypeCompleter + + +def test_LUF_completer_touch(): + c = MostRecentlyUsedFirstWordCompleter(3, ["one", "two"]) + c.touch("hello") + assert c.words == ["hello", "one", "two"] + + c.touch("foo") + assert c.words == ["foo", "hello", "one"] + + c.touch("hello") + assert c.words == ["hello", "foo", "one"] + + +def test_LUF_completer_touch_words(): + c = MostRecentlyUsedFirstWordCompleter(3, []) + c.touch_words(["hello", "world", "foo", "bar"]) + assert c.words == ["bar", "foo", "world"] + + c.touch_words(["one", "two"]) + assert c.words == ["two", "one", "bar"] + + +def test_newbie_mode_complete_without_meta_dict(): + fake_document = MagicMock() + fake_document.text_before_cursor = fake_document.text = "GEOR" + completer = IRedisCompleter(hint=False) + completions = list(completer.get_completions(fake_document, None)) + assert [word.text for word in completions] == ["GEORADIUS", "GEORADIUSBYMEMBER"] + assert [word.display_meta for word in completions] == [ + FormattedText([("", "")]), + FormattedText([("", "")]), + ] + + +def test_newbie_mode_complete_with_meta_dict(): + fake_document = MagicMock() + fake_document.text_before_cursor = fake_document.text = "GEOR" + completer = IRedisCompleter(hint=True) + completions = list(completer.get_completions(fake_document, None)) + + assert sorted([completion.display_meta for completion in completions]) == [ + FormattedText( + [ + ( + "", + "Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a member", # noqa + ) + ] + ), + FormattedText( + [ + ( + "", + "Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point", # noqa + ) + ] + ), + ] + + +def test_newbie_mode_complete_with_meta_dict_command_is_lowercase(): + fake_document = MagicMock() + fake_document.text_before_cursor = fake_document.text = "geor" + completer = IRedisCompleter(hint=True) + completions = list(completer.get_completions(fake_document, None)) + + assert sorted([completion.display_meta for completion in completions]) == [ + FormattedText( + [ + ( + "", + "Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a member", # noqa + ) + ] + ), + FormattedText( + [ + ( + "", + "Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point", # noqa + ) + ] + ), + ] + + +def test_iredis_completer_update_for_response(): + c = IRedisCompleter() + c.update_completer_for_response( + "HGETALL", + (), + [ + b"Behave", + b"misbehave", + b"Interpret", + b"misinterpret", + b"Lead", + b"mislead", + b"Trust", + b"mistrust", + ], + ) + assert c.field_completer.words == ["Trust", "Lead", "Interpret", "Behave"] + + +def test_categoryname_completer_update_for_response(): + c = IRedisCompleter() + c.update_completer_for_response( + "ACL CAT", + (), + [b"scripting", b"watch"], + ) + assert sorted(c.catetoryname_completer.words) == ["scripting", "watch"] + c.update_completer_for_response( + "ACL CAT", + ("scripting"), + [b"foo", b"bar"], + ) + assert sorted(c.catetoryname_completer.words) == ["scripting", "watch"] + + +def test_completer_when_there_are_spaces_in_command(): + c = IRedisCompleter() + c.update_completer_for_response( + "ACL cat", + (), + [b"scripting", b"watch"], + ) + assert sorted(c.catetoryname_completer.words) == ["scripting", "watch"] + + c.update_completer_for_response( + "acl \t cat", + (), + [b"hello", b"world"], + ) + assert sorted(c.catetoryname_completer.words) == [ + "hello", + "scripting", + "watch", + "world", + ] + + +def test_iredis_completer_no_exception_for_none_response(): + c = IRedisCompleter() + c.update_completer_for_response("XPENDING", None, None) + c.update_completer_for_response("KEYS", None, None) + + +def test_group_completer(): + fake_document = MagicMock() + previous_commands = ["xgroup create abc world 123", "xgroup setid abc hello 123"] + fake_document.text = fake_document.text_before_cursor = "XGROUP DESTROY key " + completer = IRedisCompleter() + for command in previous_commands: + completer.update_completer_for_input(command) + completions = list(completer.get_completions(fake_document, None)) + assert completions == [ + Completion( + text="hello", + start_position=0, + display=FormattedText([("", "hello")]), + display_meta=FormattedText([("", "")]), + ), + Completion( + text="world", + start_position=0, + display=FormattedText([("", "world")]), + display_meta=FormattedText([("", "")]), + ), + ] + + +@patch("iredis.completers.pendulum.now") +def test_timestamp_completer_humanize_time_completion(fake_now): + fake_now.return_value = pendulum.from_timestamp(1578487013) + c = TimestampCompleter() + + fake_document = MagicMock() + fake_document.text = fake_document.text_before_cursor = "30" + completions = list(c.get_completions(fake_document, None)) + + assert completions == [ + Completion( + text="1575895013000", + start_position=-2, + display=FormattedText([("", "1575895013000")]), + display_meta="30 days ago (2019-12-09 12:36:53)", + ), + Completion( + text="1578379013000", + start_position=-2, + display=FormattedText([("", "1578379013000")]), + display_meta="30 hours ago (2020-01-07 06:36:53)", + ), + Completion( + text="1578485213000", + start_position=-2, + display=FormattedText([("", "1578485213000")]), + display_meta="30 minutes ago (2020-01-08 12:06:53)", + ), + Completion( + text="1578486983000", + start_position=-2, + display=FormattedText([("", "1578486983000")]), + display_meta="30 seconds ago (2020-01-08 12:36:23)", + ), + ] + + # No plural + fake_document.text = fake_document.text_before_cursor = "1" + completions = list(c.get_completions(fake_document, None)) + + assert completions == [ + Completion( + text="1546951013000", + start_position=-1, + display=FormattedText([("", "1546951013000")]), + display_meta="1 year ago (2019-01-08 12:36:53)", + ), + Completion( + text="1575808613000", + start_position=-1, + display=FormattedText([("", "1575808613000")]), + display_meta="1 month ago (2019-12-08 12:36:53)", + ), + Completion( + text="1578400613000", + start_position=-1, + display=FormattedText([("", "1578400613000")]), + display_meta="1 day ago (2020-01-07 12:36:53)", + ), + Completion( + text="1578483413000", + start_position=-1, + display=FormattedText([("", "1578483413000")]), + display_meta="1 hour ago (2020-01-08 11:36:53)", + ), + Completion( + text="1578486953000", + start_position=-1, + display=FormattedText([("", "1578486953000")]), + display_meta="1 minute ago (2020-01-08 12:35:53)", + ), + Completion( + text="1578487012000", + start_position=-1, + display=FormattedText([("", "1578487012000")]), + display_meta="1 second ago (2020-01-08 12:36:52)", + ), + ] + + +def test_timestamp_completer_datetime_format_time_completion(): + c = TimestampCompleter() + fake_document = MagicMock() + fake_document.text = fake_document.text_before_cursor = "2020-02-07" + completions = list(c.get_completions(fake_document, None)) + + assert completions == [ + Completion( + text="1581033600000", + start_position=-10, + display=FormattedText([("", "1581033600000")]), + display_meta="2020-02-07T00:00:00+00:00", + ) + ] + + +def test_integer_type_completer(): + c = IntegerTypeCompleter() + fake_document = MagicMock() + fake_document.text = fake_document.get_word_before_cursor.return_value = "i" + completions = list(c.get_completions(fake_document, None)) + assert len(completions) == 64 + + fake_document.text = fake_document.get_word_before_cursor.return_value = "u" + completions = list(c.get_completions(fake_document, None)) + assert len(completions) == 63 + + c.touch("u4") + assert list(c.get_completions(fake_document, None))[0].text == "u4" + + +def test_completion_casing(): + c = IRedisCompleter(completion_casing="auto") + fake_document = MagicMock() + fake_document.text = fake_document.text_before_cursor = "ge" + assert [ + completion.text for completion in c.get_completions(fake_document, None) + ] == [ + "get", + "getset", + "getbit", + "geopos", + "geoadd", + "geohash", + "geodist", + "getrange", + "georadius", + "georadiusbymember", + ] + + c = IRedisCompleter(completion_casing="auto") + fake_document.text = fake_document.text_before_cursor = "GET" + assert [ + completion.text for completion in c.get_completions(fake_document, None) + ] == ["GET", "GETSET", "GETBIT", "GETRANGE"] + + c = IRedisCompleter(completion_casing="upper") + fake_document.text = fake_document.text_before_cursor = "get" + assert [ + completion.text for completion in c.get_completions(fake_document, None) + ] == ["GET", "GETSET", "GETBIT", "GETRANGE"] + + c = IRedisCompleter(completion_casing="lower") + fake_document.text = fake_document.text_before_cursor = "GET" + assert [ + completion.text for completion in c.get_completions(fake_document, None) + ] == ["get", "getset", "getbit", "getrange"] + + +def test_username_completer(): + completer = IRedisCompleter() + completer.update_completer_for_input("acl deluser laixintao") + completer.update_completer_for_input("acl deluser antirez") + + fake_document = MagicMock() + fake_document.text_before_cursor = fake_document.text = "acl deluser " + completions = list(completer.get_completions(fake_document, None)) + assert sorted([completion.text for completion in completions]) == [ + "antirez", + "laixintao", + ] + + +def test_username_touch_for_response(): + c = IRedisCompleter() + c.update_completer_for_response( + "acl users", + (), + [b"hello", b"world"], + ) + assert sorted(c.username_completer.words) == [ + "hello", + "world", + ] diff --git a/tests/unittests/test_entry.py b/tests/unittests/test_entry.py new file mode 100644 index 0000000..bfc7bec --- /dev/null +++ b/tests/unittests/test_entry.py @@ -0,0 +1,272 @@ +import pytest +import tempfile +from unittest.mock import patch +from prompt_toolkit.formatted_text import FormattedText + +from iredis.entry import ( + gather_args, + parse_url, + SkipAuthFileHistory, + write_result, + is_too_tall, +) + +from iredis.utils import DSN + + +@pytest.mark.parametrize( + "is_tty,raw_arg_is_raw,final_config_is_raw", + [ + (True, None, False), + (True, True, True), + (True, False, False), + (False, None, True), + (False, True, True), + (False, False, True), # not tty + ], +) +def test_command_entry_tty(is_tty, raw_arg_is_raw, final_config_is_raw, config): + # is tty + raw -> raw + with patch("sys.stdout.isatty") as patch_tty: + + patch_tty.return_value = is_tty + if raw_arg_is_raw is None: + call = ["iredis"] + elif raw_arg_is_raw is True: + call = ["iredis", "--raw"] + elif raw_arg_is_raw is False: + call = ["iredis", "--no-raw"] + else: + raise Exception() + gather_args.main(call, standalone_mode=False) + assert config.raw == final_config_is_raw + + +def test_disable_pager(): + from iredis.config import config + + gather_args.main(["iredis", "--decode", "utf-8"], standalone_mode=False) + assert config.enable_pager + + gather_args.main(["iredis", "--no-pager"], standalone_mode=False) + assert not config.enable_pager + + +def test_command_with_decode_utf_8(): + from iredis.config import config + + gather_args.main(["iredis", "--decode", "utf-8"], standalone_mode=False) + assert config.decode == "utf-8" + + gather_args.main(["iredis"], standalone_mode=False) + assert config.decode == "" + + +def test_command_with_shell_pipeline(): + from iredis.config import config + + gather_args.main(["iredis", "--no-shell"], standalone_mode=False) + assert config.shell is False + + gather_args.main(["iredis"], standalone_mode=False) + assert config.shell is True + + +def test_command_shell_options_higher_priority(): + from iredis.config import config + from textwrap import dedent + + config_content = dedent( + """ + [main] + shell = False + """ + ) + with open("/tmp/iredisrc", "w+") as etc_config: + etc_config.write(config_content) + + gather_args.main(["iredis", "--iredisrc", "/tmp/iredisrc"], standalone_mode=False) + assert config.shell is False + + gather_args.main( + ["iredis", "--shell", "--iredisrc", "/tmp/iredisrc"], standalone_mode=False + ) + assert config.shell is True + + +@pytest.mark.parametrize( + "url,dsn", + [ + ( + "redis://localhost:6379/3", + DSN( + scheme="redis", + host="localhost", + port=6379, + path=None, + db=3, + username=None, + password=None, + ), + ), + ( + "redis://localhost:6379", + DSN( + scheme="redis", + host="localhost", + port=6379, + path=None, + db=0, + username=None, + password=None, + ), + ), + ( + "rediss://localhost:6379", + DSN( + scheme="rediss", + host="localhost", + port=6379, + path=None, + db=0, + username=None, + password=None, + ), + ), + ( + "redis://username:password@localhost:6379", + DSN( + scheme="redis", + host="localhost", + port=6379, + path=None, + db=0, + username="username", + password="password", + ), + ), + ( + "redis://:password@localhost:6379", + DSN( + scheme="redis", + host="localhost", + port=6379, + path=None, + db=0, + username=None, + password="password", + ), + ), + ( + "redis://username@localhost:12345", + DSN( + scheme="redis", + host="localhost", + port=12345, + path=None, + db=0, + username="username", + password=None, + ), + ), + ( + # query string won't work for redis:// + "redis://username@localhost:6379?db=2", + DSN( + scheme="redis", + host="localhost", + port=6379, + path=None, + db=0, + username="username", + password=None, + ), + ), + ( + "unix://username:password2@/tmp/to/socket.sock?db=0", + DSN( + scheme="unix", + host=None, + port=None, + path="/tmp/to/socket.sock", + db=0, + username="username", + password="password2", + ), + ), + ( + "unix://:password3@/path/to/socket.sock", + DSN( + scheme="unix", + host=None, + port=None, + path="/path/to/socket.sock", + db=0, + username=None, + password="password3", + ), + ), + ( + "unix:///tmp/socket.sock", + DSN( + scheme="unix", + host=None, + port=None, + path="/tmp/socket.sock", + db=0, + username=None, + password=None, + ), + ), + ], +) +def test_parse_url(url, dsn): + assert parse_url(url) == dsn + + +@pytest.mark.parametrize( + "command,record", + [ + ("set foo bar", True), + ("set auth bar", True), + ("auth 123", False), + ("AUTH hello", False), + ("AUTH hello world", False), + ], +) +def test_history(command, record): + f = tempfile.TemporaryFile("w+") + history = SkipAuthFileHistory(f.name) + assert history._loaded_strings == [] + history.append_string(command) + assert (command in history._loaded_strings) is record + + +def test_write_result_for_str(capsys): + write_result("hello") + captured = capsys.readouterr() + assert captured.out == "hello\n" + + +def test_write_result_for_bytes(capsys): + write_result(b"hello") + captured = capsys.readouterr() + assert captured.out == "hello\n" + + +def test_write_result_for_formatted_text(): + ft = FormattedText([("class:keyword", "set"), ("class:string", "hello world")]) + # just this test not raise means ok + write_result(ft) + + +def test_is_too_tall_for_formatted_text(): + ft = FormattedText([("class:key", f"key-{index}\n") for index in range(21)]) + assert is_too_tall(ft, 20) + assert not is_too_tall(ft, 22) + + +def test_is_too_tall_for_bytes(): + byte_text = b"".join([b"key\n" for index in range(21)]) + assert is_too_tall(byte_text, 20) + assert not is_too_tall(byte_text, 23) diff --git a/tests/unittests/test_markdown_doc_render.py b/tests/unittests/test_markdown_doc_render.py new file mode 100644 index 0000000..0538299 --- /dev/null +++ b/tests/unittests/test_markdown_doc_render.py @@ -0,0 +1,28 @@ +""" +This test ensures that all of redis-doc's markdown can be rendered. +Why do we need this? +see: +https://github.com/antirez/redis-doc/commit/02b3d1a345093c1794fd86273e9d516fffd3b819 +""" + +import pytest +from importlib_resources import read_text + +from iredis.commands import commands_summary +from iredis.data import commands as commands_data +from iredis.markdown import render + + +doc_files = [] +for command, info in commands_summary.items(): + command_docs_name = "-".join(command.split()).lower() + if info["group"] == "iredis": + continue + doc_files.append(f"{command_docs_name}.md") + + +@pytest.mark.parametrize("filename", doc_files) +def test_markdown_render(filename): + print(filename) + doc = read_text(commands_data, filename) + render(doc) diff --git a/tests/unittests/test_render_functions.py b/tests/unittests/test_render_functions.py new file mode 100644 index 0000000..2da9b35 --- /dev/null +++ b/tests/unittests/test_render_functions.py @@ -0,0 +1,486 @@ +import os +import time +from prompt_toolkit.formatted_text import FormattedText +from iredis import renders +from iredis.config import config +from iredis.completers import IRedisCompleter + + +def strip_formatted_text(formatted_text): + return "".join(text[1] for text in formatted_text) + + +def test_render_simple_string_raw_using_raw_render(): + assert renders.OutputRender.render_raw(b"OK") == b"OK" + assert renders.OutputRender.render_raw(b"BUMPED 1") == b"BUMPED 1" + assert renders.OutputRender.render_raw(b"STILL 1") == b"STILL 1" + + +def test_render_simple_string(): + assert renders.OutputRender.render_simple_string(b"OK") == FormattedText( + [("class:success", "OK")] + ) + assert renders.OutputRender.render_simple_string(b"BUMPED 1") == FormattedText( + [("class:success", "BUMPED 1")] + ) + assert renders.OutputRender.render_simple_string(b"STILL 1") == FormattedText( + [("class:success", "STILL 1")] + ) + + +def test_render_list_index(): + raw = ["hello", "world", "foo"] + out = renders._render_list([item.encode() for item in raw], raw) + out = strip_formatted_text(out) + assert isinstance(out, str) + assert "3)" in out + assert "1)" in out + assert "4)" not in out + + +def test_render_list_index_const_width(): + raw = ["hello"] * 100 + out = renders._render_list([item.encode() for item in raw], raw) + out = strip_formatted_text(out) + assert isinstance(out, str) + assert " 1)" in out + assert "\n100)" in out + + raw = ["hello"] * 1000 + out = renders._render_list([item.encode() for item in raw], raw) + out = strip_formatted_text(out) + assert " 1)" in out + assert "\n 999)" in out + assert "\n1000)" in out + + raw = ["hello"] * 10 + out = renders._render_list([item.encode() for item in raw], raw) + out = strip_formatted_text(out) + assert " 1)" in out + assert "\n 9)" in out + assert "\n10)" in out + + +def test_render_list_using_raw_render(): + raw = ["hello", "world", "foo"] + out = renders.OutputRender.render_raw([item.encode() for item in raw]) + assert b"hello\nworld\nfoo" == out + + +def test_render_list_with_nil_init(): + raw = [b"hello", None, b"world"] + out = renders.OutputRender.render_list(raw) + out = strip_formatted_text(out) + assert out == '1) "hello"\n2) (nil)\n3) "world"' + + +def test_render_list_with_nil_init_while_config_raw(): + raw = [b"hello", None, b"world"] + out = renders.OutputRender.render_raw(raw) + assert out == b"hello\n\nworld" + + +def test_render_list_with_empty_list_raw(): + raw = [] + out = renders.OutputRender.render_raw(raw) + assert out == b"" + + +def test_render_list_with_empty_list(): + raw = [] + out = renders.OutputRender.render_list(raw) + out = strip_formatted_text(out) + assert out == "(empty list or set)" + + +def test_ensure_str_bytes(): + assert renders.ensure_str(b"hello world") == r"hello world" + assert renders.ensure_str(b"hello'world") == r"hello'world" + assert renders.ensure_str("你好".encode()) == r"\xe4\xbd\xa0\xe5\xa5\xbd" + + +def test_double_quotes(): + assert renders.double_quotes('hello"world') == r'"hello\"world"' + assert renders.double_quotes('"hello\\world"') == '"\\"hello\\world\\""' + + assert renders.double_quotes("'") == '"\'"' + assert renders.double_quotes("\\") == '"\\"' + assert renders.double_quotes('"') == '"\\""' + + +def test_render_int(): + config.raw = False + assert renders.OutputRender.render_int(12) == FormattedText( + [("class:type", "(integer) "), ("", "12")] + ) + + +def test_render_int_raw(): + assert renders.OutputRender.render_raw(12) == b"12" + + +def test_render_list_or_string(): + assert renders.OutputRender.render_list_or_string("") == '""' + assert renders.OutputRender.render_list_or_string("foo") == '"foo"' + assert renders.OutputRender.render_list_or_string( + [b"foo", b"bar"] + ) == FormattedText( + [ + ("", "1)"), + ("", " "), + ("class:string", '"foo"'), + ("", "\n"), + ("", "2)"), + ("", " "), + ("class:string", '"bar"'), + ] + ) + + +def test_render_list_or_string_nil_and_empty_list(): + assert renders.OutputRender.render_list_or_string(None) == FormattedText( + [("class:type", "(nil)")] + ) + assert renders.OutputRender.render_list_or_string([]) == FormattedText( + [("class:type", "(empty list or set)")] + ) + + +def test_render_raw_nil_and_empty_list(): + assert renders.OutputRender.render_raw(None) == b"" + assert renders.OutputRender.render_raw([]) == b"" + + +def test_list_or_string(): + config.raw = False + assert renders.OutputRender.render_string_or_int(b"10.1") == '"10.1"' + assert renders.OutputRender.render_string_or_int(3) == FormattedText( + [("class:type", "(integer) "), ("", "3")] + ) + + +def test_command_keys(): + completer = IRedisCompleter() + completer.key_completer.words = [] + config.raw = False + rendered = renders.OutputRender.command_keys([b"cat", b"dog", b"banana"]) + completer.update_completer_for_response("KEYS", None, [b"cat", b"dog", b"banana"]) + + assert rendered == FormattedText( + [ + ("", "1)"), + ("", " "), + ("class:key", '"cat"'), + ("", "\n"), + ("", "2)"), + ("", " "), + ("class:key", '"dog"'), + ("", "\n"), + ("", "3)"), + ("", " "), + ("class:key", '"banana"'), + ] + ) + assert completer.key_completer.words == ["banana", "dog", "cat"] + + +def test_command_scan(): + completer = IRedisCompleter() + completer.key_completer.words = [] + config.raw = False + rendered = renders.OutputRender.command_scan( + [b"44", [b"a", b"key:__rand_int__", b"dest", b" a"]] + ) + completer.update_completer_for_response( + "SCAN", ("0",), [b"44", [b"a", b"key:__rand_int__", b"dest", b" a"]] + ) + + assert rendered == FormattedText( + [ + ("class:type", "(cursor) "), + ("class:integer", "44"), + ("", "\n"), + ("", "1)"), + ("", " "), + ("class:key", '"a"'), + ("", "\n"), + ("", "2)"), + ("", " "), + ("class:key", '"key:__rand_int__"'), + ("", "\n"), + ("", "3)"), + ("", " "), + ("class:key", '"dest"'), + ("", "\n"), + ("", "4)"), + ("", " "), + ("class:key", '" a"'), + ] + ) + assert completer.key_completer.words == [" a", "dest", "key:__rand_int__", "a"] + + +def test_command_sscan(): + completer = IRedisCompleter() + completer.member_completer.words = [] + rendered = renders.OutputRender.command_sscan( + [b"44", [b"a", b"member:__rand_int__", b"dest", b" a"]] + ) + completer.update_completer_for_response( + "SSCAN", (0), [b"44", [b"a", b"member:__rand_int__", b"dest", b" a"]] + ) + + assert rendered == FormattedText( + [ + ("class:type", "(cursor) "), + ("class:integer", "44"), + ("", "\n"), + ("", "1)"), + ("", " "), + ("class:member", '"a"'), + ("", "\n"), + ("", "2)"), + ("", " "), + ("class:member", '"member:__rand_int__"'), + ("", "\n"), + ("", "3)"), + ("", " "), + ("class:member", '"dest"'), + ("", "\n"), + ("", "4)"), + ("", " "), + ("class:member", '" a"'), + ] + ) + assert completer.member_completer.words == [ + " a", + "dest", + "member:__rand_int__", + "a", + ] + + +def test_command_sscan_config_raw(): + completer = IRedisCompleter() + completer.member_completer.words = [] + rendered = renders.OutputRender.render_raw( + [b"44", [b"a", b"member:__rand_int__", b"dest", b" a"]] + ) + completer.update_completer_for_response( + "SSCAN", (0), [b"44", [b"a", b"member:__rand_int__", b"dest", b" a"]] + ) + + assert rendered == b"44\na\nmember:__rand_int__\ndest\n a" + assert completer.member_completer.words == [ + " a", + "dest", + "member:__rand_int__", + "a", + ] + + +def test_render_members(): + completer = IRedisCompleter() + completer.member_completer.words = [] + config.withscores = True + resp = [b"duck", b"667", b"camel", b"708"] + rendered = renders.OutputRender.render_members(resp) + completer.update_completer_for_response("ZRANGE", ("foo", "0", "-1"), resp) + + assert rendered == FormattedText( + [ + ("", "1)"), + ("", " "), + ("class:integer", "667 "), + ("class:member", '"duck"'), + ("", "\n"), + ("", "2)"), + ("", " "), + ("class:integer", "708 "), + ("class:member", '"camel"'), + ] + ) + assert completer.member_completer.words == ["camel", "duck"] + + +def test_render_members_config_raw(): + completer = IRedisCompleter() + completer.member_completer.words = [] + config.withscores = True + resp = [b"duck", b"667", b"camel", b"708"] + rendered = renders.OutputRender.render_raw(resp) + completer.update_completer_for_response("ZRANGE", (), resp) + + assert rendered == b"duck\n667\ncamel\n708" + assert completer.member_completer.words == ["camel", "duck"] + + +def test_render_unixtime_config_raw(): + # fake the timezone and reload + os.environ["TZ"] = "Asia/Shanghai" + time.tzset() + rendered = renders.OutputRender.render_unixtime(1570469891) + + assert rendered == FormattedText( + [ + ("class:type", "(integer) "), + ("", "1570469891"), + ("", "\n"), + ("class:type", "(local time)"), + ("", " "), + ("", "2019-10-08 01:38:11"), + ] + ) + + +def test_render_unixtime(): + rendered = renders.OutputRender.render_raw(1570469891) + + assert rendered == b"1570469891" + + +def test_bulk_string_reply(): + assert renders.OutputRender.render_bulk_string(b"'\"") == '''"'\\""''' + + +def test_bulk_string_reply_raw(): + assert renders.OutputRender.render_raw(b"hello") == b"hello" + + +def test_render_bulk_string_decoded(): + EXPECTED_RENDER = """# Server\nredis_version:5.0.5\nredis_git_sha1:00000000\nredis_git_dirty:0\nredis_build_id:31cd6e21ec924b46""" # noqa + _input = b"# Server\r\nredis_version:5.0.5\r\nredis_git_sha1:00000000\r\nredis_git_dirty:0\r\nredis_build_id:31cd6e21ec924b46" # noqa + assert renders.OutputRender.render_bulk_string_decode(_input) == EXPECTED_RENDER + + +def test_render_bulk_string_decoded_with_decoded_utf8(): + EXPECTED_RENDER = """# Server\nredis_version:5.0.5\nredis_git_sha1:00000000\nredis_git_dirty:0\nredis_build_id:31cd6e21ec924b46""" # noqa + _input = "# Server\r\nredis_version:5.0.5\r\nredis_git_sha1:00000000\r\nredis_git_dirty:0\r\nredis_build_id:31cd6e21ec924b46" # noqa + assert renders.OutputRender.render_bulk_string_decode(_input) == EXPECTED_RENDER + + +def test_render_time(): + value = [b"1571305643", b"765481"] + assert renders.OutputRender.render_time(value) == FormattedText( + [ + ("class:type", "(unix timestamp) "), + ("", "1571305643"), + ("", "\n"), + ("class:type", "(millisecond) "), + ("", "765481"), + ("", "\n"), + ("class:type", "(convert to local timezone) "), + ("", "2019-10-17 17:47:23.765481"), + ] + ) + + assert renders.OutputRender.render_raw(value) == b"1571305643\n765481" + + +def test_render_nested_pairs(): + text = [ + b"peak.allocated", + 10160336, + b"lua.caches", + 0, + b"db.0", + [b"overhead.hashtable.main", 648, b"overhead.hashtable.expires", 32], + b"db.1", + [b"overhead.hashtable.main", 112, b"overhead.hashtable.expires", 32], + b"fragmentation", + b"0.062980629503726959", + b"fragmentation.bytes", + -9445680, + ] + + assert renders.OutputRender.render_raw(text) == ( + b"peak.allocated\n10160336\nlua.caches\n0\ndb.0\noverhead.hashtable.main\n64" + b"8\noverhead.hashtable.expires\n32\ndb.1\noverhead.hashtable.main\n112\nove" + b"rhead.hashtable.expires\n32\nfragmentation\n0.062980629503726959\nfragmentat" + b"ion.bytes\n-9445680" + ) + + assert renders.OutputRender.render_nested_pair(text) == FormattedText( + [ + ("class:string", "peak.allocated: "), + ("class:value", "10160336"), + ("", "\n"), + ("class:string", "lua.caches: "), + ("class:value", "0"), + ("", "\n"), + ("class:string", "db.0: "), + ("", "\n"), + ("class:string", " overhead.hashtable.main: "), + ("class:value", "648"), + ("", "\n"), + ("class:string", " overhead.hashtable.expires: "), + ("class:value", "32"), + ("", "\n"), + ("class:string", "db.1: "), + ("", "\n"), + ("class:string", " overhead.hashtable.main: "), + ("class:value", "112"), + ("", "\n"), + ("class:string", " overhead.hashtable.expires: "), + ("class:value", "32"), + ("", "\n"), + ("class:string", "fragmentation: "), + ("class:value", "0.062980629503726959"), + ("", "\n"), + ("class:string", "fragmentation.bytes: "), + ("class:value", "-9445680"), + ] + ) + + +def test_render_nested_list(): + text = [[b"get", 2, [b"readonly", b"fast"], 1, 1, 1]] + assert renders.OutputRender.render_list(text) == FormattedText( + [ + ("", "1)"), + ("", " "), + ("", "1)"), + ("", " "), + ("class:string", '"get"'), + ("", "\n"), + ("", " "), + ("", "2)"), + ("", " "), + ("class:string", '"2"'), + ("", "\n"), + ("", " "), + ("", "3)"), + ("", " "), + ("", "1)"), + ("", " "), + ("class:string", '"readonly"'), + ("", "\n"), + ("", " "), + ("", "2)"), + ("", " "), + ("class:string", '"fast"'), + ("", "\n"), + ("", " "), + ("", "4)"), + ("", " "), + ("class:string", '"1"'), + ("", "\n"), + ("", " "), + ("", "5)"), + ("", " "), + ("class:string", '"1"'), + ("", "\n"), + ("", " "), + ("", "6)"), + ("", " "), + ("class:string", '"1"'), + ] + ) + + +def test_render_bytes(config): + assert renders.OutputRender.render_bytes(b"bytes\n") == b"bytes" + + +def test_render_bytes_raw(config): + assert renders.OutputRender.render_raw(b"bytes\n") == b"bytes\n" diff --git a/tests/unittests/test_utils.py b/tests/unittests/test_utils.py new file mode 100644 index 0000000..c9b5eff --- /dev/null +++ b/tests/unittests/test_utils.py @@ -0,0 +1,122 @@ +import re +import time +import pytest +from unittest.mock import patch + +from iredis.utils import timer, strip_quote_args +from iredis.commands import split_command_args, split_unknown_args +from iredis.utils import command_syntax +from iredis.style import STYLE +from iredis.exceptions import InvalidArguments, AmbiguousCommand +from iredis.commands import commands_summary +from prompt_toolkit import print_formatted_text + + +def test_timer(): + with patch("iredis.utils.logger") as mock_logger: + timer("foo") + time.sleep(0.1) + timer("bar") + mock_logger.debug.assert_called() + args, kwargs = mock_logger.debug.call_args + matched = re.match(r"\[timer (\d)\] (0\.\d+) -> bar", args[0]) + + assert matched.group(1) == str(3) + assert 0.1 <= float(matched.group(2)) <= 0.2 + + # --- test again --- + timer("foo") + time.sleep(0.2) + timer("bar") + mock_logger.debug.assert_called() + args, kwargs = mock_logger.debug.call_args + matched = re.match(r"\[timer (\d)\] (0\.\d+) -> bar", args[0]) + + assert matched.group(1) == str(5) + assert 0.2 <= float(matched.group(2)) <= 0.3 + + +@pytest.mark.parametrize( + "test_input,expected", + [ + ("hello world", ["hello", "world"]), + ("'hello world'", ["hello world"]), + ('''hello"world"''', ["helloworld"]), + (r'''hello\"world"''', [r"hello\world"]), + (r'"\\"', [r"\\"]), + (r"\\", [r"\\"]), + (r"\abcd ef", [r"\abcd", "ef"]), + # quotes in quotes + (r""" 'hello"world' """, ['hello"world']), + (r""" "hello'world" """, ["hello'world"]), + (r""" 'hello\'world'""", ["hello'world"]), + (r""" "hello\"world" """, ['hello"world']), + (r"''", [""]), # set foo "" is a legal command + (r'""', [""]), # set foo "" is a legal command + (r"\\", ["\\\\"]), # blackslash are legal + ("\\hello\\", ["\\hello\\"]), # blackslash are legal + ], +) +def test_stipe_quote_escaple_in_quote(test_input, expected): + assert list(strip_quote_args(test_input)) == expected + + +@pytest.mark.parametrize( + "command,expected,args", + [ + ("GET a", "GET", ["a"]), + ("cluster info", "cluster info", []), + ("getbit foo 17", "getbit", ["foo", "17"]), + ("command ", "command", []), + (" command count ", "command count", []), + (" command count ", "command count", []), # command with multi space + (" command count ' hello world'", "command count", [" hello world"]), + ("set foo 'hello world'", "set", ["foo", "hello world"]), + ], +) +def test_split_commands(command, expected, args): + parsed_command, parsed_args = split_command_args(command) + assert expected == parsed_command + assert args == parsed_args + + +def test_split_commands_fail_on_unknown_command(): + with pytest.raises(InvalidArguments): + split_command_args("FOO BAR") + + +@pytest.mark.parametrize( + "command", + ["command in", "command in", "Command in", "COMMAND in"], +) +def test_split_commands_fail_on_partially_input(command): + with pytest.raises(AmbiguousCommand): + split_command_args(command) + + +def test_split_commands_fail_on_unfinished_command(): + with pytest.raises(InvalidArguments): + split_command_args("setn") + + +def test_render_bottom_with_command_json(): + for command, info in commands_summary.items(): + print_formatted_text(command_syntax(command, info), style=STYLE) + + +@pytest.mark.parametrize( + "raw,command,args", + [ + ("abc 123", "abc", ["123"]), + ("abc", "abc", []), + ("abc foo bar", "abc", ["foo", "bar"]), + ("abc 'foo bar'", "abc", ["foo bar"]), + ('abc "foo bar"', "abc", ["foo bar"]), + ('abc "foo bar" 3 hello', "abc", ["foo bar", "3", "hello"]), + ('abc "foo \nbar"', "abc", ["foo \nbar"]), + ], +) +def test_split_unknown_commands(raw, command, args): + parsed_command, parsed_args = split_unknown_args(raw) + assert command == parsed_command + assert args == parsed_args -- cgit v1.2.3