From 8fa230c50d3404fa08e4e22e28d1c2eb1b03ba1c Mon Sep 17 00:00:00 2001 From: Aryan Pardeshi Date: Sun, 9 Aug 2026 03:03:19 +0530 Subject: [PATCH 1/3] test: add CLI integration coverage for rvl index, stats and version commands --- tests/integration/test_cli_integration.py | 107 ++++++++++++++++++++++ uv.lock | 2 +- 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 tests/integration/test_cli_integration.py diff --git a/tests/integration/test_cli_integration.py b/tests/integration/test_cli_integration.py new file mode 100644 index 00000000..36d23a3d --- /dev/null +++ b/tests/integration/test_cli_integration.py @@ -0,0 +1,107 @@ +import os +import subprocess +import sys +import tempfile +import yaml +import pytest + + +@pytest.fixture +def cli_schema_file(redis_test_name): + """Creates a temporary schema YAML file for integration testing CLI commands.""" + index_name = redis_test_name("cli_int_index") + prefix = redis_test_name("cli_int_doc") + schema_data = { + "index": { + "name": index_name, + "prefix": prefix, + "storage_type": "hash", + }, + "fields": [ + {"name": "title", "type": "text"}, + {"name": "tag", "type": "tag"}, + ], + } + with tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False) as f: + yaml.dump(schema_data, f) + temp_path = f.name + + yield temp_path, index_name + + if os.path.exists(temp_path): + os.remove(temp_path) + + +def run_cli(*args, redis_url=None): + """Helper to execute rvl CLI commands via subprocess runner.""" + cmd = [sys.executable, "-m", "redisvl.cli.runner"] + list(args) + env = os.environ.copy() + if redis_url: + env["REDIS_URL"] = redis_url + result = subprocess.run( + cmd, + capture_output=True, + text=True, + env=env, + check=False, + ) + return result + + +def test_cli_version(): + res = run_cli("version") + assert res.returncode == 0 + assert "redisvl" in res.stdout.lower() or "version" in res.stdout.lower() or "." in res.stdout + + +def test_cli_index_lifecycle(redis_url, cli_schema_file): + schema_path, index_name = cli_schema_file + + # 1. Create index + create_res = run_cli("index", "create", "-s", schema_path, "--url", redis_url) + assert create_res.returncode == 0, f"create failed: {create_res.stderr}" + + # 2. List all indexes + list_res = run_cli("index", "listall", "--url", redis_url) + assert list_res.returncode == 0, f"listall failed: {list_res.stderr}" + assert index_name in list_res.stdout + + # 3. Get index info + info_res = run_cli("index", "info", "-i", index_name, "--url", redis_url) + assert info_res.returncode == 0, f"info failed: {info_res.stderr}" + assert index_name in info_res.stdout + + # 4. Get index stats + stats_res = run_cli("stats", "-i", index_name, "--url", redis_url) + assert stats_res.returncode == 0, f"stats failed: {stats_res.stderr}" + assert index_name in stats_res.stdout or "num_docs" in stats_res.stdout or "STAT" in stats_res.stdout + + # 5. Delete index + delete_res = run_cli("index", "delete", "-i", index_name, "--url", redis_url) + assert delete_res.returncode == 0, f"delete failed: {delete_res.stderr}" + + # 6. Re-create index to test destroy + create_res2 = run_cli("index", "create", "-s", schema_path, "--url", redis_url) + assert create_res2.returncode == 0, f"second create failed: {create_res2.stderr}" + + # 7. Destroy index (with --drop / clear keys) + destroy_res = run_cli("index", "destroy", "-i", index_name, "--url", redis_url) + assert destroy_res.returncode == 0, f"destroy failed: {destroy_res.stderr}" + + +def test_cli_error_paths(redis_url): + # Non-existent schema file + schema_res = run_cli("index", "create", "-s", "non_existent_file_xyz.yaml", "--url", redis_url) + assert schema_res.returncode != 0 + + # Info for non-existent index + info_res = run_cli("index", "info", "-i", "non_existent_index_xyz_9999", "--url", redis_url) + assert info_res.returncode != 0 + + # Stats for non-existent index + stats_res = run_cli("stats", "-i", "non_existent_index_xyz_9999", "--url", redis_url) + assert stats_res.returncode != 0 + + # Invalid CLI subcommand + invalid_res = run_cli("invalid_command_name") + assert invalid_res.returncode == 2 diff --git a/uv.lock b/uv.lock index 8936cf40..210d0618 100644 --- a/uv.lock +++ b/uv.lock @@ -4841,7 +4841,7 @@ wheels = [ [[package]] name = "redisvl" -version = "0.25.0" +version = "0.25.1" source = { editable = "." } dependencies = [ { name = "jsonpath-ng" }, From 6e0338493c41e104c43e45da3045d682f69f6bad Mon Sep 17 00:00:00 2001 From: Aryan Pardeshi Date: Sun, 9 Aug 2026 11:37:48 +0530 Subject: [PATCH 2/3] test: force utf-8 in CLI subprocess so box-drawing output does not break on non-utf8 consoles --- tests/integration/test_cli_integration.py | 32 ++++++++++++++++++----- uv.lock | 2 +- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/tests/integration/test_cli_integration.py b/tests/integration/test_cli_integration.py index 36d23a3d..88156c53 100644 --- a/tests/integration/test_cli_integration.py +++ b/tests/integration/test_cli_integration.py @@ -2,8 +2,9 @@ import subprocess import sys import tempfile -import yaml + import pytest +import yaml @pytest.fixture @@ -38,10 +39,15 @@ def run_cli(*args, redis_url=None): env = os.environ.copy() if redis_url: env["REDIS_URL"] = redis_url + # `rvl index info` renders a box-drawing table. Without this the child + # process inherits a non-UTF-8 stdout encoding on some platforms and dies + # with a UnicodeEncodeError before printing anything. + env["PYTHONIOENCODING"] = "utf-8" result = subprocess.run( cmd, capture_output=True, text=True, + encoding="utf-8", env=env, check=False, ) @@ -51,7 +57,11 @@ def run_cli(*args, redis_url=None): def test_cli_version(): res = run_cli("version") assert res.returncode == 0 - assert "redisvl" in res.stdout.lower() or "version" in res.stdout.lower() or "." in res.stdout + assert ( + "redisvl" in res.stdout.lower() + or "version" in res.stdout.lower() + or "." in res.stdout + ) def test_cli_index_lifecycle(redis_url, cli_schema_file): @@ -74,7 +84,11 @@ def test_cli_index_lifecycle(redis_url, cli_schema_file): # 4. Get index stats stats_res = run_cli("stats", "-i", index_name, "--url", redis_url) assert stats_res.returncode == 0, f"stats failed: {stats_res.stderr}" - assert index_name in stats_res.stdout or "num_docs" in stats_res.stdout or "STAT" in stats_res.stdout + assert ( + index_name in stats_res.stdout + or "num_docs" in stats_res.stdout + or "STAT" in stats_res.stdout + ) # 5. Delete index delete_res = run_cli("index", "delete", "-i", index_name, "--url", redis_url) @@ -91,15 +105,21 @@ def test_cli_index_lifecycle(redis_url, cli_schema_file): def test_cli_error_paths(redis_url): # Non-existent schema file - schema_res = run_cli("index", "create", "-s", "non_existent_file_xyz.yaml", "--url", redis_url) + schema_res = run_cli( + "index", "create", "-s", "non_existent_file_xyz.yaml", "--url", redis_url + ) assert schema_res.returncode != 0 # Info for non-existent index - info_res = run_cli("index", "info", "-i", "non_existent_index_xyz_9999", "--url", redis_url) + info_res = run_cli( + "index", "info", "-i", "non_existent_index_xyz_9999", "--url", redis_url + ) assert info_res.returncode != 0 # Stats for non-existent index - stats_res = run_cli("stats", "-i", "non_existent_index_xyz_9999", "--url", redis_url) + stats_res = run_cli( + "stats", "-i", "non_existent_index_xyz_9999", "--url", redis_url + ) assert stats_res.returncode != 0 # Invalid CLI subcommand diff --git a/uv.lock b/uv.lock index 210d0618..8936cf40 100644 --- a/uv.lock +++ b/uv.lock @@ -4841,7 +4841,7 @@ wheels = [ [[package]] name = "redisvl" -version = "0.25.1" +version = "0.25.0" source = { editable = "." } dependencies = [ { name = "jsonpath-ng" }, From f116821be827660f479a3302a5e2864652198905 Mon Sep 17 00:00:00 2001 From: Aryan Pardeshi Date: Mon, 10 Aug 2026 20:13:49 +0530 Subject: [PATCH 3/3] fix: assert index removal after delete/destroy and tear down leftover indices test_cli_index_lifecycle only checked exit codes after delete and destroy, so a no-op that exits 0 against an already-gone index would have passed silently. Assert listall no longer shows the index after each, and that the re-create step in between actually recreated it rather than being a no-op against a still-existing one. cli_schema_file's teardown only removed the temp YAML file and never touched the Redis index itself, so a test failing partway through the lifecycle left the index behind for the next run. Added an unconditional destroy in teardown, safe because destroy on an already-gone index is a no-op from the CLI's own error handling. --- tests/integration/test_cli_integration.py | 30 ++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/integration/test_cli_integration.py b/tests/integration/test_cli_integration.py index 88156c53..c222663f 100644 --- a/tests/integration/test_cli_integration.py +++ b/tests/integration/test_cli_integration.py @@ -8,7 +8,7 @@ @pytest.fixture -def cli_schema_file(redis_test_name): +def cli_schema_file(redis_url, redis_test_name): """Creates a temporary schema YAML file for integration testing CLI commands.""" index_name = redis_test_name("cli_int_index") prefix = redis_test_name("cli_int_doc") @@ -32,6 +32,13 @@ def cli_schema_file(redis_test_name): if os.path.exists(temp_path): os.remove(temp_path) + # Belt-and-suspenders index teardown: a test that fails midway through the + # lifecycle (e.g. the assertion after `delete` never runs) must not leave + # the index behind for the next test run. `destroy` on an already-gone + # index is a no-op from the CLI's own error handling, so this is safe to + # run unconditionally. + run_cli("index", "destroy", "-i", index_name, "--url", redis_url) + def run_cli(*args, redis_url=None): """Helper to execute rvl CLI commands via subprocess runner.""" @@ -94,14 +101,35 @@ def test_cli_index_lifecycle(redis_url, cli_schema_file): delete_res = run_cli("index", "delete", "-i", index_name, "--url", redis_url) assert delete_res.returncode == 0, f"delete failed: {delete_res.stderr}" + list_after_delete = run_cli("index", "listall", "--url", redis_url) + assert list_after_delete.returncode == 0, list_after_delete.stderr + assert index_name not in list_after_delete.stdout, ( + "delete exited 0 but the index is still listed -- a no-op delete " + "against an already-gone index would pass this test if it only " + "checked the exit code" + ) + # 6. Re-create index to test destroy create_res2 = run_cli("index", "create", "-s", schema_path, "--url", redis_url) assert create_res2.returncode == 0, f"second create failed: {create_res2.stderr}" + list_after_recreate = run_cli("index", "listall", "--url", redis_url) + assert index_name in list_after_recreate.stdout, ( + "second create exited 0 but the index isn't listed -- confirms the " + "index from step 5 was actually gone rather than the create being a " + "no-op against a still-existing index" + ) + # 7. Destroy index (with --drop / clear keys) destroy_res = run_cli("index", "destroy", "-i", index_name, "--url", redis_url) assert destroy_res.returncode == 0, f"destroy failed: {destroy_res.stderr}" + list_after_destroy = run_cli("index", "listall", "--url", redis_url) + assert list_after_destroy.returncode == 0, list_after_destroy.stderr + assert ( + index_name not in list_after_destroy.stdout + ), "destroy exited 0 but the index is still listed" + def test_cli_error_paths(redis_url): # Non-existent schema file