From aa4c8a29ce622e069facc7bdc8070d6aef88cbca Mon Sep 17 00:00:00 2001 From: Roman Solomatin <36135455+Samoed@users.noreply.github.com> Date: Mon, 6 Apr 2026 09:29:50 +0300 Subject: [PATCH 1/5] move tests to tests folder --- pyproject.toml | 3 +++ python/{scripts => tests}/test_distances.py | 0 python/{scripts => tests}/test_index.py | 0 python/{scripts => tests}/test_jit.py | 0 python/{scripts => tests}/test_sparse.py | 0 python/{scripts => tests}/test_sqlite.py | 0 python/{scripts => tests}/test_tooling.py | 0 7 files changed, 3 insertions(+) rename python/{scripts => tests}/test_distances.py (100%) rename python/{scripts => tests}/test_index.py (100%) rename python/{scripts => tests}/test_jit.py (100%) rename python/{scripts => tests}/test_sparse.py (100%) rename python/{scripts => tests}/test_sqlite.py (100%) rename python/{scripts => tests}/test_tooling.py (100%) diff --git a/pyproject.toml b/pyproject.toml index b871556ad..0f9e8123c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,9 @@ addopts = "-ra --showlocals --strict-markers --strict-config -s -x -p no:warning filterwarnings = ["error"] minversion = "6.0" xfail_strict = true +testpaths = [ + "python/tests", +] # Avoid running tests, as everything is happening in a super slow container # We have already run all the relavent Python tests in `prerelease.yml` diff --git a/python/scripts/test_distances.py b/python/tests/test_distances.py similarity index 100% rename from python/scripts/test_distances.py rename to python/tests/test_distances.py diff --git a/python/scripts/test_index.py b/python/tests/test_index.py similarity index 100% rename from python/scripts/test_index.py rename to python/tests/test_index.py diff --git a/python/scripts/test_jit.py b/python/tests/test_jit.py similarity index 100% rename from python/scripts/test_jit.py rename to python/tests/test_jit.py diff --git a/python/scripts/test_sparse.py b/python/tests/test_sparse.py similarity index 100% rename from python/scripts/test_sparse.py rename to python/tests/test_sparse.py diff --git a/python/scripts/test_sqlite.py b/python/tests/test_sqlite.py similarity index 100% rename from python/scripts/test_sqlite.py rename to python/tests/test_sqlite.py diff --git a/python/scripts/test_tooling.py b/python/tests/test_tooling.py similarity index 100% rename from python/scripts/test_tooling.py rename to python/tests/test_tooling.py From d7dc1aab02280a0fe1ae72060c7fe2a97f9e67a8 Mon Sep 17 00:00:00 2001 From: Roman Solomatin <36135455+Samoed@users.noreply.github.com> Date: Mon, 6 Apr 2026 09:45:26 +0300 Subject: [PATCH 2/5] fix pyproject --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 793168cc6..c31341ae2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,6 @@ requires = [ addopts = "-ra --showlocals --strict-markers --strict-config -s -x -p no:warnings" filterwarnings = ["error"] minversion = "6.0" -testpaths = ["python/scripts"] xfail_strict = true testpaths = [ "python/tests", From 028b3a13e0353cfe4ff920a2f762b9d7c48a1a19 Mon Sep 17 00:00:00 2001 From: Roman Solomatin <36135455+Samoed@users.noreply.github.com> Date: Sun, 3 May 2026 22:23:12 +0300 Subject: [PATCH 3/5] add more tests --- pyproject.toml | 3 +- python/tests/test_distances.py | 2 +- python/tests/test_index.py | 119 ++++++++++++++++++++++++++++++++- python/tests/test_jit.py | 4 +- python/tests/test_sqlite.py | 4 +- python/tests/test_tooling.py | 2 +- 6 files changed, 123 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c31341ae2..e4f4b3c71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ requires = [ ] [tool.pytest.ini_options] -addopts = "-ra --showlocals --strict-markers --strict-config -s -x -p no:warnings" +addopts = "-ra --showlocals --strict-markers --strict-config -s -x -p no:warnings --cov=usearch --cov-report=term-missing" filterwarnings = ["error"] minversion = "6.0" xfail_strict = true @@ -95,6 +95,7 @@ lint = [ tests = [ "numpy>=1.21", "pytest>=9.0.2", + "pytest-cov>=7.1.0", "pytest-repeat>=0.9.4", ] diff --git a/python/tests/test_distances.py b/python/tests/test_distances.py index eb64d46e1..8d01a0ee0 100644 --- a/python/tests/test_distances.py +++ b/python/tests/test_distances.py @@ -7,7 +7,7 @@ Usage: uv run python/scripts/test_distances.py - + Dependencies listed in the script header for uv to resolve automatically. """ # /// script diff --git a/python/tests/test_index.py b/python/tests/test_index.py index a55c74358..8d0ba237a 100644 --- a/python/tests/test_index.py +++ b/python/tests/test_index.py @@ -435,6 +435,119 @@ def test_index_keys_iteration(): assert keys_list[0] == 42 +@pytest.mark.parametrize("ndim", [16, 64]) +@pytest.mark.parametrize("batch_size", [10, 50]) +def test_index_join(ndim, batch_size): + """Semantic join should return a 1-to-1 mapping between two indexes.""" + index_a = Index(ndim=ndim, metric=MetricKind.Cos) + index_b = Index(ndim=ndim, metric=MetricKind.Cos) + + vectors_a = random_vectors(count=batch_size, ndim=ndim) + vectors_b = random_vectors(count=batch_size, ndim=ndim) + keys_a = np.arange(batch_size) + keys_b = np.arange(batch_size, 2 * batch_size) + + index_a.add(keys_a, vectors_a) + index_b.add(keys_b, vectors_b) + + mapping = index_a.join(index_b, exact=True) + assert isinstance(mapping, dict) + assert len(mapping) > 0 + # All returned keys must be valid + assert all(k in keys_a for k in mapping.keys()) + assert all(v in keys_b for v in mapping.values()) + # No two a-keys should map to the same b-key (stable marriage property) + assert len(set(mapping.values())) == len(mapping) + + +def test_index_ip_metric(): + """Inner product metric should be usable and produce valid searches.""" + ndim = 32 + count = 20 + index = Index(ndim=ndim, metric=MetricKind.IP) + keys = np.arange(count) + vectors = random_vectors(count=count, ndim=ndim, metric=MetricKind.IP) + index.add(keys, vectors) + + matches = index.search(vectors[0], 5) + assert isinstance(matches, Matches) + assert len(matches) == 5 + + +def test_index_specs(): + """specs property should return a dict with expected keys.""" + ndim = 16 + index = Index(ndim=ndim, metric=MetricKind.L2sq, dtype=ScalarKind.F32) + s = index.specs + assert isinstance(s, dict) + for key in ("ndim", "multi", "connectivity", "expansion_add", "expansion_search", "dtype"): + assert key in s + assert s["ndim"] == ndim + + +@pytest.mark.parametrize("ndim", [16, 64]) +@pytest.mark.parametrize("batch_size", [10, 50]) +def test_index_exact_search(ndim, batch_size): + """Exact search must return the query vector itself as the top match.""" + index = Index(ndim=ndim, metric=MetricKind.L2sq) + keys = np.arange(batch_size) + vectors = random_vectors(count=batch_size, ndim=ndim) + index.add(keys, vectors) + + if batch_size == 1: + matches = index.search(vectors, 1, exact=True) + assert int(matches.keys[0]) == keys[0] + else: + matches: BatchMatches = index.search(vectors, 1, exact=True) + top_keys = [int(m.keys[0]) for m in matches] + assert top_keys == list(keys) + + +@pytest.mark.parametrize("batch_size", [6, 20]) +def test_index_pairwise_distance_array(batch_size): + """pairwise_distance with equal-length key arrays returns element-wise distances.""" + ndim = 16 + index = Index(ndim=ndim, metric=MetricKind.L2sq) + keys = np.arange(batch_size) + vectors = random_vectors(count=batch_size, ndim=ndim) + index.add(keys, vectors) + + half = batch_size // 2 + left_keys = keys[:half] + right_keys = keys[half : 2 * half] + distances = index.pairwise_distance(left_keys, right_keys) + assert distances.shape == (half,) + assert np.all(distances >= 0) + + +def test_index_pairwise_distance_scalar(): + """pairwise_distance with scalar keys returns a scalar distance.""" + ndim = 16 + index = Index(ndim=ndim, metric=MetricKind.L2sq) + keys = np.arange(4) + vectors = random_vectors(count=4, ndim=ndim) + index.add(keys, vectors) + + dist = index.pairwise_distance(0, 1) + assert isinstance(dist, float) + assert dist >= 0 + # Distance from a vector to itself should be ~0 + assert index.pairwise_distance(0, 0) == pytest.approx(0.0, abs=1e-3) + + +def test_index_memory_usage_grows(): + """Memory usage should increase as vectors are added.""" + ndim = 32 + index = Index(ndim=ndim) + mem_empty = index.memory_usage + + keys = np.arange(100) + vectors = random_vectors(count=100, ndim=ndim) + index.add(keys, vectors) + + assert index.memory_usage > mem_empty + + def test_index_copied_memory_usage(): """Test that copy=False results in lower memory usage than copy=True.""" reset_randomness() @@ -460,6 +573,6 @@ def test_index_copied_memory_usage(): memory_with_copy = index_copied.memory_usage memory_without_copy = index_viewing.memory_usage - assert ( - memory_with_copy > memory_without_copy - ), f"Expected default index addition to use more memory than copy=False ({memory_with_copy} vs {memory_without_copy})" + assert memory_with_copy > memory_without_copy, ( + f"Expected default index addition to use more memory than copy=False ({memory_with_copy} vs {memory_without_copy})" + ) diff --git a/python/tests/test_jit.py b/python/tests/test_jit.py index b1c1c3783..31f6841e5 100644 --- a/python/tests/test_jit.py +++ b/python/tests/test_jit.py @@ -221,9 +221,7 @@ def test_index_cppyy(ndim: int, batch_size: int): result += a[i] * b[i]; return 1 - result; } - """.replace( - "ndim", str(ndim) - ) + """.replace("ndim", str(ndim)) ) functions = [ diff --git a/python/tests/test_sqlite.py b/python/tests/test_sqlite.py index 627319b83..96a736551 100644 --- a/python/tests/test_sqlite.py +++ b/python/tests/test_sqlite.py @@ -7,13 +7,13 @@ Usage: uv run python/scripts/test_sqlite.py - + Dependencies listed in the script header for uv to resolve automatically. """ # /// script # dependencies = [ # "pytest", -# "numpy", +# "numpy", # "usearch" # ] # /// diff --git a/python/tests/test_tooling.py b/python/tests/test_tooling.py index d73577bb1..ef0f9214f 100644 --- a/python/tests/test_tooling.py +++ b/python/tests/test_tooling.py @@ -7,7 +7,7 @@ Usage: uv run python/scripts/test_tooling.py - + Dependencies listed in the script header for uv to resolve automatically. """ # /// script From 3091e65a478af9f80a54c2cd7da54f0ec2e6e1a4 Mon Sep 17 00:00:00 2001 From: Roman Solomatin <36135455+Samoed@users.noreply.github.com> Date: Sun, 3 May 2026 22:40:06 +0300 Subject: [PATCH 4/5] add dependency group --- .github/workflows/prerelease.yml | 3 +-- pyproject.toml | 8 ++++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index afcdf8746..e486db0ec 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -261,8 +261,7 @@ jobs: - name: Build Python run: | python -m pip install --upgrade pip - pip install pytest pytest-repeat numpy - python -m pip install . + python -m pip install . --group tests env: CXX: clang++ CC: clang++ # Override the default compiler diff --git a/pyproject.toml b/pyproject.toml index a62e7b2db..1af84fe18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,14 @@ requires = [ "numkong>=7.5.0", ] +[dependency-groups] +tests = [ + "pytest", + "pytest-repeat", + "numpy", + "pytest-cov", +] + [tool.pytest.ini_options] addopts = "-ra --showlocals --strict-markers --strict-config -s -x -p no:warnings --cov=usearch --cov-report=term-missing" filterwarnings = ["error"] From 2319cb74d592590714e276da06ee010a0fb724cf Mon Sep 17 00:00:00 2001 From: Roman Solomatin <36135455+Samoed@users.noreply.github.com> Date: Sun, 3 May 2026 22:44:22 +0300 Subject: [PATCH 5/5] remove cov --- pyproject.toml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1af84fe18..0b0205efc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,16 +16,8 @@ requires = [ "numkong>=7.5.0", ] -[dependency-groups] -tests = [ - "pytest", - "pytest-repeat", - "numpy", - "pytest-cov", -] - [tool.pytest.ini_options] -addopts = "-ra --showlocals --strict-markers --strict-config -s -x -p no:warnings --cov=usearch --cov-report=term-missing" +addopts = "-ra --showlocals --strict-markers --strict-config -s -x -p no:warnings" filterwarnings = ["error"] minversion = "6.0" xfail_strict = true