Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/scripts/ci_changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ def classify(paths):
selected.add("python_quality")
elif path == "tests/test_pyright.py":
selected.update(("python_quality", "python_build"))
elif path == "tests/test_mypy.py":
selected.update(("python_quality", "python_build", "wheels"))
elif path.startswith("tests/python/"):
selected.update(("python_build", "wheels"))
if path == "tests/python/conftest.py":
Expand Down
53 changes: 28 additions & 25 deletions .github/scripts/test_ci_workflow_contracts.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
"""Check lightweight CI coverage and workflow scheduling contracts."""

import fnmatch
import json
import os
import re
import subprocess
import sys
import tempfile
import textwrap
import tomllib
import unittest
from pathlib import Path
Expand All @@ -23,7 +23,28 @@ def test_installed_typing_checks_are_required(self):
wheel_dependencies = config["tool"]["cibuildwheel"]["test-requires"]
checker = [value for value in test_dependencies if value.startswith("mypy==")]
self.assertEqual(len(checker), 1)
self.assertIn(checker[0], wheel_dependencies)
self.assertFalse(any(value.startswith("mypy") for value in wheel_dependencies))
wheels = config["tool"]["cibuildwheel"]
typing_overrides = [
o for o in wheels["overrides"] if checker[0] in o.get("test-requires", [])
]
self.assertEqual(len(typing_overrides), 1)
override = typing_overrides[0]
self.assertEqual(override["inherit"]["test-requires"], "append")
self.assertEqual(override["inherit"]["test-command"], "append")
self.assertIn("pytest {project}/tests/test_mypy.py", override["test-command"])
self.assertEqual(
{
identifier
for identifier in wheels["build"].split()
if fnmatch.fnmatchcase(identifier, override["select"])
},
{f"cp{abi}-manylinux_x86_64" for abi in (311, 312, 313, 314)},
)
self.assertIn(
"--ignore={project}/tests/python/test_benchmark_protocol.py",
wheels["test-command"],
)
self.assertFalse(
any(value.startswith("mypy") for value in config["project"]["dependencies"])
)
Expand All @@ -32,7 +53,10 @@ def test_installed_typing_checks_are_required(self):
" python-result:\n", 1
)[0]
self.assertIn('pip install ".[test,typing]"', build)
self.assertIn("python -m pytest tests/python/test_editor_support.py", build)
self.assertIn(
"python -m pytest tests/python/test_editor_support.py tests/test_mypy.py",
build,
)
self.assertNotIn("continue-on-error", build)
self.assertIn(
"pytest {project}/tests/python",
Expand All @@ -45,33 +69,12 @@ def test_installed_typing_checks_are_required(self):
for path in (
"python_bindings/__init__.pyi",
"tests/python/typing/public_api.py",
"tests/test_mypy.py",
):
with self.subTest(path=path):
self.assertLessEqual(
{"python_build", "python_quality", "wheels"}, classify([path])
)
# Exercise the actual aggregator script, so a failed selected matrix
# cannot be reported as a successful Python validation check.
result = workflow.split(" python-result:\n", 1)[1].split(
" python-windows:\n", 1
)[0]
self.assertIn("needs: [changes, python-build]", result)
script = textwrap.dedent(result.split(" run: |\n", 1)[1])
for matrix_result in ("success", "failure", "cancelled", "skipped"):
with self.subTest(matrix_result=matrix_result):
checked = subprocess.run(
["bash", "-e", "-c", script],
env=dict(
os.environ,
CHANGES_RESULT="success",
PYTHON_SELECTED="true",
MATRIX_RESULT=matrix_result,
),
capture_output=True,
text=True,
check=False,
)
self.assertEqual(checked.returncode == 0, matrix_result == "success")

def test_pyright_checks_are_required_and_scoped(self):
config = tomllib.loads((ROOT / "pyproject.toml").read_text())
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ jobs:
- name: Check installed Python types and editor metadata
env:
OMP_NUM_THREADS: "2"
run: python -m pytest tests/python/test_editor_support.py --tb=short -q
run: python -m pytest tests/python/test_editor_support.py tests/test_mypy.py --tb=short -q

- name: Check Pyright public API and test code
run: python -m pytest tests/test_pyright.py --tb=short -q
Expand Down
13 changes: 11 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,17 @@ cmake.args = [
build = "cp311-manylinux_x86_64 cp312-manylinux_x86_64 cp313-manylinux_x86_64 cp314-manylinux_x86_64 cp311-manylinux_aarch64 cp312-manylinux_aarch64 cp313-manylinux_aarch64 cp314-manylinux_aarch64 cp311-win_amd64 cp312-win_amd64 cp313-win_amd64 cp314-win_amd64 cp311-macosx_arm64 cp312-macosx_arm64 cp313-macosx_arm64 cp314-macosx_arm64"
build-verbosity = 1
environment = { CMAKE_ARGS = "-DRABITQ_ENABLE_NATIVE_OPTIMIZATION=OFF" }
test-requires = ["pytest", "mypy==2.4.0"]
test-command = "pytest {project}/tests/python --ignore={project}/tests/python/test_compare_with_faiss.py --ignore={project}/tests/python/test_cpp_clustering_examples.py --tb=short -q"
test-requires = ["pytest"]
test-command = "pytest {project}/tests/python --ignore={project}/tests/python/test_compare_with_faiss.py --ignore={project}/tests/python/test_cpp_clustering_examples.py --ignore={project}/tests/python/test_benchmark_protocol.py --tb=short -q"

# Static typing runs once per wheel ABI. Every platform still checks its installed
# declarations, runtime signatures, documentation and package metadata.
[[tool.cibuildwheel.overrides]]
select = "*-manylinux_x86_64"
inherit.test-requires = "append"
test-requires = ["mypy==2.4.0"]
inherit.test-command = "append"
test-command = "pytest {project}/tests/test_mypy.py --tb=short -q"

[tool.cibuildwheel.linux]
manylinux-aarch64-image = "manylinux_2_28"
Expand Down
2 changes: 1 addition & 1 deletion scripts/check-python.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ if [[ "$($ruff --version)" != "ruff 0.16.1" ]]; then
fi

cd "$repo_root"
paths=(python_bindings sample/python tests/python tests/test_pyright.py benchmarks)
paths=(python_bindings sample/python tests/python tests/test_mypy.py tests/test_pyright.py benchmarks)
"$ruff" format --check "${paths[@]}"
"$ruff" check "${paths[@]}"
10 changes: 7 additions & 3 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,12 +332,16 @@ Array shapes and value ranges remain runtime checks.
After rebuilding and installing with the `test` extra, run:

```bash
python -m pytest tests/python/test_editor_support.py -q
python -m pytest tests/python/test_editor_support.py tests/test_mypy.py -q
```

The suite checks installed declarations, signatures, documentation, examples, and
valid/invalid calls with strict mypy. CI requires it; missing mypy fails the test.
Keep the mypy pins in the `test` extra and wheel test requirements synchronized.
runtime metadata on every wheel. Strict mypy valid/invalid-call checks live in
`tests/test_mypy.py`, run for source installs and each CPython ABI on Linux x86-64
wheels. Other wheel platforms do not repeat static analysis. CI requires mypy
for these selected checks; missing mypy fails the test. Keep its pins in the
`test` extra and Linux x86-64 wheel override synchronized.
Benchmark protocol tests run in Python quality CI, separately from wheel tests.

CI also checks the public type examples and test code with Pyright. With Node 24
and the `test,typing` extras installed in the same environment, run:
Expand Down
73 changes: 0 additions & 73 deletions tests/common/test_data.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#include "test_data.hpp"

#include <cmath>
#include <cstddef>
#include <random>
#include <vector>
Expand All @@ -20,76 +19,4 @@ std::vector<float> TestDataGenerator::GenerateRandomVector(
return vec;
}

std::vector<float> TestDataGenerator::GenerateNormalizedVector(
size_t dim, unsigned int seed
) {
auto vec = GenerateRandomVector(dim, -1.0f, 1.0f, seed);

// Calculate L2 norm
float norm = 0.0f;
for (float val : vec) {
norm += val * val;
}
norm = std::sqrt(norm);

// Normalize
if (norm > 1e-10f) {
for (float& val : vec) {
val /= norm;
}
}

return vec;
}

std::vector<std::vector<float>> TestDataGenerator::GenerateRandomVectors(
size_t num_vectors, size_t dim, float min, float max, unsigned int seed
) {
std::vector<std::vector<float>> vectors;
vectors.reserve(num_vectors);

for (size_t i = 0; i < num_vectors; ++i) {
vectors.push_back(GenerateRandomVector(dim, min, max, seed + i));
}

return vectors;
}

std::vector<float> TestDataGenerator::GenerateGaussianVector(
size_t dim, float mean, float stddev, unsigned int seed
) {
std::mt19937 rng(seed);
std::normal_distribution<float> dist(mean, stddev);

std::vector<float> vec(dim);
for (size_t i = 0; i < dim; ++i) {
vec[i] = dist(rng);
}
return vec;
}

std::vector<float> TestDataGenerator::GenerateSimpleVector(size_t dim) {
std::vector<float> vec(dim);
for (size_t i = 0; i < dim; ++i) {
vec[i] = static_cast<float>(i % 10) / 10.0f;
}
return vec;
}

std::vector<float> TestDataGenerator::GenerateZeroVector(size_t dim) {
return std::vector<float>(dim, 0.0f);
}

std::vector<float> TestDataGenerator::GenerateOnesVector(size_t dim) {
return std::vector<float>(dim, 1.0f);
}

std::vector<float> TestDataGenerator::GenerateIncrementalVector(size_t dim) {
std::vector<float> vec(dim);
for (size_t i = 0; i < dim; ++i) {
vec[i] = static_cast<float>(i);
}
return vec;
}

} // namespace rabitq_test
29 changes: 0 additions & 29 deletions tests/common/test_data.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,6 @@ class TestDataGenerator {
static std::vector<float> GenerateRandomVector(
size_t dim, float min = -1.0f, float max = 1.0f, unsigned int seed = 42
);

// Generate random normalized vector (unit length)
static std::vector<float> GenerateNormalizedVector(size_t dim, unsigned int seed = 42);

// Generate multiple random vectors
static std::vector<std::vector<float>> GenerateRandomVectors(
size_t num_vectors,
size_t dim,
float min = -1.0f,
float max = 1.0f,
unsigned int seed = 42
);

// Generate Gaussian distributed vector
static std::vector<float> GenerateGaussianVector(
size_t dim, float mean = 0.0f, float stddev = 1.0f, unsigned int seed = 42
);

// Generate a simple test vector with known values
static std::vector<float> GenerateSimpleVector(size_t dim);

// Generate zero vector
static std::vector<float> GenerateZeroVector(size_t dim);

// Generate vector with all ones
static std::vector<float> GenerateOnesVector(size_t dim);

// Generate vector with incremental values [0, 1, 2, 3, ...]
static std::vector<float> GenerateIncrementalVector(size_t dim);
};

} // namespace rabitq_test
Expand Down
64 changes: 0 additions & 64 deletions tests/common/test_helpers.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
#include <gtest/gtest.h>

#include <cmath>
#include <cstddef>

namespace rabitq_test {

Expand All @@ -13,74 +12,11 @@ inline bool FloatNearlyEqual(float a, float b, float epsilon = 1e-5f) {
return std::abs(a - b) < epsilon;
}

inline bool DoubleNearlyEqual(double a, double b, double epsilon = 1e-10) {
return std::abs(a - b) < epsilon;
}

// Vector comparison
inline bool VectorsNearlyEqual(
const float* a, const float* b, size_t size, float epsilon = 1e-5f
) {
for (size_t i = 0; i < size; ++i) {
if (!FloatNearlyEqual(a[i], b[i], epsilon)) {
return false;
}
}
return true;
}

// Calculate relative error
inline float RelativeError(float actual, float expected) {
if (std::abs(expected) < 1e-10f) {
return std::abs(actual - expected);
}
return std::abs((actual - expected) / expected);
}

// Calculate mean squared error
inline float MeanSquaredError(const float* a, const float* b, size_t size) {
float mse = 0.0f;
for (size_t i = 0; i < size; ++i) {
float diff = a[i] - b[i];
mse += diff * diff;
}
return mse / static_cast<float>(size);
}

// Calculate dot product
inline float DotProduct(const float* a, const float* b, size_t size) {
float result = 0.0f;
for (size_t i = 0; i < size; ++i) {
result += a[i] * b[i];
}
return result;
}

// Calculate L2 distance
inline float L2Distance(const float* a, const float* b, size_t size) {
float sum = 0.0f;
for (size_t i = 0; i < size; ++i) {
float diff = a[i] - b[i];
sum += diff * diff;
}
return std::sqrt(sum);
}

// Custom assertion macros
#define ASSERT_FLOAT_NEARLY_EQUAL(a, b, epsilon) \
ASSERT_TRUE(rabitq_test::FloatNearlyEqual((a), (b), (epsilon))) \
<< "Expected: " << (a) << " to be nearly equal to " << (b) \
<< " (epsilon: " << (epsilon) << "), but difference was " << std::abs((a) - (b))

#define EXPECT_FLOAT_NEARLY_EQUAL(a, b, epsilon) \
EXPECT_TRUE(rabitq_test::FloatNearlyEqual((a), (b), (epsilon))) \
<< "Expected: " << (a) << " to be nearly equal to " << (b) \
<< " (epsilon: " << (epsilon) << "), but difference was " << std::abs((a) - (b))

#define ASSERT_VECTORS_NEARLY_EQUAL(a, b, size, epsilon) \
ASSERT_TRUE(rabitq_test::VectorsNearlyEqual((a), (b), (size), (epsilon))) \
<< "Vectors are not nearly equal (epsilon: " << (epsilon) << ")"

} // namespace rabitq_test

#endif // RABITQ_TEST_HELPERS_HPP
Loading
Loading