From 3c5d00404bb92452e7a10f574815099b25bc02d6 Mon Sep 17 00:00:00 2001 From: Rafik Saliev Date: Fri, 27 Mar 2026 16:07:40 +0100 Subject: [PATCH 1/7] Add comprehensive C API tests for error handling, index building, search parameters, and storage - Introduced tests for error handling in the C API, ensuring proper creation, cleanup, and state management of error handles. - Implemented tests for index building and searching, covering various scenarios including different metrics, storage types, and threadpool configurations. - Added tests for search parameters creation and validation, including handling of invalid sizes. - Developed tests for storage creation, validating various data types and ensuring proper error handling for invalid arguments. - Ensured all tests utilize the Catch2 framework for consistency and clarity. --- bindings/c/CMakeLists.txt | 7 +- bindings/c/src/svs_c.cpp | 18 +- bindings/c/tests/CMakeLists.txt | 86 ++++ bindings/c/tests/README.md | 164 +++++++ bindings/c/tests/c_api_algorithm.cpp | 199 ++++++++ bindings/c/tests/c_api_dynamic_index.cpp | 419 ++++++++++++++++ bindings/c/tests/c_api_error.cpp | 79 +++ bindings/c/tests/c_api_index.cpp | 583 +++++++++++++++++++++++ bindings/c/tests/c_api_index_builder.cpp | 226 +++++++++ bindings/c/tests/c_api_search_params.cpp | 88 ++++ bindings/c/tests/c_api_storage.cpp | 181 +++++++ 11 files changed, 2044 insertions(+), 6 deletions(-) create mode 100644 bindings/c/tests/CMakeLists.txt create mode 100644 bindings/c/tests/README.md create mode 100644 bindings/c/tests/c_api_algorithm.cpp create mode 100644 bindings/c/tests/c_api_dynamic_index.cpp create mode 100644 bindings/c/tests/c_api_error.cpp create mode 100644 bindings/c/tests/c_api_index.cpp create mode 100644 bindings/c/tests/c_api_index_builder.cpp create mode 100644 bindings/c/tests/c_api_search_params.cpp create mode 100644 bindings/c/tests/c_api_storage.cpp diff --git a/bindings/c/CMakeLists.txt b/bindings/c/CMakeLists.txt index e8e220ee..aa56ddbb 100644 --- a/bindings/c/CMakeLists.txt +++ b/bindings/c/CMakeLists.txt @@ -191,8 +191,9 @@ install(FILES ) # Build tests if requested -# if(SVS_BUILD_C_API_TESTS) -# add_subdirectory(tests) -# endif() +option(SVS_BUILD_C_API_TESTS "Build C API tests" ON) +if(SVS_BUILD_C_API_TESTS) + add_subdirectory(tests) +endif() add_subdirectory(samples) diff --git a/bindings/c/src/svs_c.cpp b/bindings/c/src/svs_c.cpp index 85f2fa3e..d67523dc 100644 --- a/bindings/c/src/svs_c.cpp +++ b/bindings/c/src/svs_c.cpp @@ -661,20 +661,32 @@ extern "C" size_t svs_index_dynamic_delete_points( svs_index_h index, const size_t* ids, size_t num_ids, svs_error_h out_err ) { using namespace svs::c_runtime; - return wrap_exceptions( + std::shared_ptr dynamic_index_ptr; + auto result = wrap_exceptions( [&]() { EXPECT_ARG_NOT_NULL(index); EXPECT_ARG_NOT_NULL(ids); EXPECT_ARG_GT_THAN(num_ids, 0); - auto dynamic_index_ptr = std::dynamic_pointer_cast(index->impl); + dynamic_index_ptr = std::dynamic_pointer_cast(index->impl); INVALID_ARGUMENT_IF( dynamic_index_ptr == nullptr, "Index does not support dynamic updates" ); - return dynamic_index_ptr->delete_points(std::span(ids, num_ids)); + return 0; // return 0 for success, actual deletion happens in the next + // wrap_exceptions call }, out_err, static_cast(-1) ); + if (result != 0) { + return result; + } + // Call delete_points in a separate wrap_exceptions to return 0 if no entries are + // deleted. + return wrap_exceptions( + [&]() { return dynamic_index_ptr->delete_points(std::span(ids, num_ids)); }, + out_err, + 0 + ); } extern "C" bool svs_index_dynamic_has_id( diff --git a/bindings/c/tests/CMakeLists.txt b/bindings/c/tests/CMakeLists.txt new file mode 100644 index 00000000..5085c0ed --- /dev/null +++ b/bindings/c/tests/CMakeLists.txt @@ -0,0 +1,86 @@ +# Copyright 2026 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Check if Catch2 is available +find_package(Catch2 3 QUIET) + +if(NOT Catch2_FOUND) + message(STATUS "Catch2 not found, fetching from GitHub...") + include(FetchContent) + + # Do wide printing for the console logger for Catch2 + set(CATCH_CONFIG_CONSOLE_WIDTH "100" CACHE STRING "" FORCE) + set(CATCH_BUILD_TESTING OFF CACHE BOOL "" FORCE) + set(CATCH_CONFIG_ENABLE_BENCHMARKING OFF CACHE BOOL "" FORCE) + set(CATCH_CONFIG_FAST_COMPILE OFF CACHE BOOL "" FORCE) + set(CATCH_CONFIG_PREFIX_ALL ON CACHE BOOL "" FORCE) + + FetchContent_Declare( + Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v3.11.0 + ) + FetchContent_MakeAvailable(Catch2) +endif() + +# Define test sources +set(C_API_TEST_SOURCES + c_api_error.cpp + c_api_algorithm.cpp + c_api_storage.cpp + c_api_search_params.cpp + c_api_index_builder.cpp + c_api_index.cpp + c_api_dynamic_index.cpp +) + +# Create test executable +add_executable(svs_c_api_tests ${C_API_TEST_SOURCES}) + +# Link with C API library and Catch2 +target_link_libraries(svs_c_api_tests PRIVATE + svs_c_api + Catch2::Catch2WithMain +) + +# Set C++ standard +target_compile_features(svs_c_api_tests PRIVATE cxx_std_17) + +# Include directories +target_include_directories(svs_c_api_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../include +) + +# Add test to CTest +include(CTest) + +# Add Catch2 CMake module path +if(NOT Catch2_FOUND) + # Catch2 was fetched, use its source directory + list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras) +else() + # Catch2 was found via find_package, use its module directory + list(APPEND CMAKE_MODULE_PATH ${Catch2_DIR}) +endif() + +include(Catch) +catch_discover_tests(svs_c_api_tests) + +# Add a custom target to run tests +add_custom_target(run_c_api_tests + COMMAND svs_c_api_tests + DEPENDS svs_c_api_tests + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Running C API tests..." +) diff --git a/bindings/c/tests/README.md b/bindings/c/tests/README.md new file mode 100644 index 00000000..0a1cac58 --- /dev/null +++ b/bindings/c/tests/README.md @@ -0,0 +1,164 @@ +# C API Tests + +This directory contains comprehensive tests for the SVS C API using the Catch2 testing framework. + +## Test Structure + +The tests are organized into separate files by functionality: + +- **c_api_error.cpp**: Tests for error handling functionality +- **c_api_algorithm.cpp**: Tests for algorithm creation and configuration (Vamana) +- **c_api_storage.cpp**: Tests for storage configurations (Simple, LeanVec, LVQ, SQ) +- **c_api_search_params.cpp**: Tests for search parameter creation and configuration +- **c_api_index_builder.cpp**: Tests for index builder creation and configuration +- **c_api_index.cpp**: Tests for index building, searching, and basic operations +- **c_api_dynamic_index.cpp**: Tests for dynamic index operations (add, delete, consolidate, compact) + +Note: The main() function is provided by Catch2::Catch2WithMain automatically. + +## Building the Tests + +The tests are built as part of the C API build process. To build them: + +```bash +# From the build directory +cmake -DSVS_BUILD_C_API_TESTS=ON .. +make svs_c_api_tests +``` + +To disable building tests: + +```bash +cmake -DSVS_BUILD_C_API_TESTS=OFF .. +``` + +## Running the Tests + +### Run all tests + +```bash +./svs_c_api_tests +``` + +### Run specific test cases + +```bash +# Run error handling tests only +./svs_c_api_tests "[c_api][error]" + +# Run algorithm tests only +./svs_c_api_tests "[c_api][algorithm]" + +# Run all index tests +./svs_c_api_tests "[c_api][index]" + +# Run dynamic index tests +./svs_c_api_tests "[c_api][dynamic]" +``` + +### Run with verbose output + +```bash +./svs_c_api_tests -s +``` + +### List all available tests + +```bash +./svs_c_api_tests --list-tests +``` + +### Run with CTest + +```bash +ctest -R svs_c_api_tests +``` + +## Test Coverage + +The tests cover the following aspects of the C API: + +### Error Handling + +- Error handle creation and cleanup +- Error state checking +- Error codes and messages +- NULL error handle support + +### Algorithm Configuration + +- Vamana algorithm creation +- Parameter getters and setters (graph_degree, build_window_size, alpha, search_history) +- Invalid parameter handling + +### Storage Configuration + +- Simple storage (Float32, Float16, Int8, Uint8) +- LeanVec storage (various primary/secondary combinations) +- LVQ storage (with and without residual) +- Scalar Quantization storage + +### Search Parameters + +- Vamana search parameter creation +- Various window sizes + +### Index Builder + +- Index builder creation with different metrics (Euclidean, Cosine, Dot Product) +- Storage configuration +- Thread pool configuration (Native, OMP, Custom) + +### Index Operations + +- Index building from data +- Searching with queries +- Different K values +- Distance calculation +- Vector reconstruction +- Thread count management + +### Dynamic Index Operations + +- Dynamic index building with/without explicit IDs +- Adding points +- Deleting points +- ID existence checking +- Index consolidation +- Index compaction +- Search after modifications + +## Test Patterns + +The tests follow the patterns established in the SVS project: + +1. Use `CATCH_TEST_CASE` for test case definitions +2. Use `CATCH_SECTION` for test subsections +3. Use `CATCH_REQUIRE` for assertions +4. Clean up all resources (free handles) after each test +5. Test both success and error paths +6. Test with and without NULL error handles + +## Adding New Tests + +When adding new tests: + +1. Create a new `.cpp` file or add to an existing one +2. Follow the existing structure and naming conventions +3. Include proper copyright header +4. Use appropriate test tags: `[c_api][functionality]` +5. Add the new test file to `CMakeLists.txt` if needed +6. Clean up all allocated resources +7. Test both success and error conditions + +## Dependencies + +- Catch2 v3.x (automatically fetched if not found) +- SVS C API library +- C++17 or later compiler + +## Notes + +- Tests use a simple sequential thread pool for deterministic behavior +- Test data is generated programmatically for repeatability +- Some tests may be skipped if optional features are not enabled (e.g., LVQ/LeanVec) diff --git a/bindings/c/tests/c_api_algorithm.cpp b/bindings/c/tests/c_api_algorithm.cpp new file mode 100644 index 00000000..e72f044a --- /dev/null +++ b/bindings/c/tests/c_api_algorithm.cpp @@ -0,0 +1,199 @@ +/* + * Copyright 2026 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// C API +#include "svs/c_api/svs_c.h" + +// catch2 +#include "catch2/catch_test_macros.hpp" + +// Standard library +#include + +CATCH_TEST_CASE("C API Vamana Algorithm", "[c_api][algorithm][vamana]") { + CATCH_SECTION("Vamana Algorithm Creation") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(64, 128, 100, error); + CATCH_REQUIRE(algorithm != nullptr); + CATCH_REQUIRE(svs_error_ok(error) == true); + + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Vamana Algorithm Get Graph Degree") { + svs_error_h error = svs_error_create(); + size_t expected_degree = 64; + + svs_algorithm_h algorithm = + svs_algorithm_create_vamana(expected_degree, 128, 100, error); + CATCH_REQUIRE(algorithm != nullptr); + + size_t actual_degree = 0; + bool success = + svs_algorithm_vamana_get_graph_degree(algorithm, &actual_degree, error); + CATCH_REQUIRE(success == true); + CATCH_REQUIRE(svs_error_ok(error) == true); + CATCH_REQUIRE(actual_degree == expected_degree); + + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Vamana Algorithm Set Graph Degree") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(64, 128, 100, error); + CATCH_REQUIRE(algorithm != nullptr); + + size_t new_degree = 96; + bool success = svs_algorithm_vamana_set_graph_degree(algorithm, new_degree, error); + CATCH_REQUIRE(success == true); + CATCH_REQUIRE(svs_error_ok(error) == true); + + size_t actual_degree = 0; + success = svs_algorithm_vamana_get_graph_degree(algorithm, &actual_degree, error); + CATCH_REQUIRE(success == true); + CATCH_REQUIRE(actual_degree == new_degree); + + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Vamana Algorithm Get Build Window Size") { + svs_error_h error = svs_error_create(); + size_t expected_window = 128; + + svs_algorithm_h algorithm = + svs_algorithm_create_vamana(64, expected_window, 100, error); + CATCH_REQUIRE(algorithm != nullptr); + + size_t actual_window = 0; + bool success = + svs_algorithm_vamana_get_build_window_size(algorithm, &actual_window, error); + CATCH_REQUIRE(success == true); + CATCH_REQUIRE(svs_error_ok(error) == true); + CATCH_REQUIRE(actual_window == expected_window); + + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Vamana Algorithm Set Build Window Size") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(64, 128, 100, error); + CATCH_REQUIRE(algorithm != nullptr); + + size_t new_window = 256; + bool success = + svs_algorithm_vamana_set_build_window_size(algorithm, new_window, error); + CATCH_REQUIRE(success == true); + CATCH_REQUIRE(svs_error_ok(error) == true); + + size_t actual_window = 0; + success = + svs_algorithm_vamana_get_build_window_size(algorithm, &actual_window, error); + CATCH_REQUIRE(success == true); + CATCH_REQUIRE(actual_window == new_window); + + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Vamana Algorithm Get/Set Alpha") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(64, 128, 100, error); + CATCH_REQUIRE(algorithm != nullptr); + + // Get default alpha + float alpha = 0.0f; + bool success = svs_algorithm_vamana_get_alpha(algorithm, &alpha, error); + CATCH_REQUIRE(success == true); + CATCH_REQUIRE(svs_error_ok(error) == true); + CATCH_REQUIRE(alpha > 0.0f); + + // Set new alpha + float new_alpha = 1.5f; + success = svs_algorithm_vamana_set_alpha(algorithm, new_alpha, error); + CATCH_REQUIRE(success == true); + CATCH_REQUIRE(svs_error_ok(error) == true); + + // Verify the change + float actual_alpha = 0.0f; + success = svs_algorithm_vamana_get_alpha(algorithm, &actual_alpha, error); + CATCH_REQUIRE(success == true); + CATCH_REQUIRE(std::abs(actual_alpha - new_alpha) < 1e-6f); + + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Vamana Algorithm Get/Set Search History") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(64, 128, 100, error); + CATCH_REQUIRE(algorithm != nullptr); + + // Get default search history setting + bool use_history = false; + bool success = + svs_algorithm_vamana_get_use_search_history(algorithm, &use_history, error); + CATCH_REQUIRE(success == true); + CATCH_REQUIRE(svs_error_ok(error) == true); + + // Set search history + bool new_value = !use_history; + success = svs_algorithm_vamana_set_use_search_history(algorithm, new_value, error); + CATCH_REQUIRE(success == true); + CATCH_REQUIRE(svs_error_ok(error) == true); + + // Verify the change + bool actual_value = false; + success = + svs_algorithm_vamana_get_use_search_history(algorithm, &actual_value, error); + CATCH_REQUIRE(success == true); + CATCH_REQUIRE(actual_value == new_value); + + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Vamana Algorithm with NULL Error") { + svs_algorithm_h algorithm = svs_algorithm_create_vamana(64, 128, 100, nullptr); + CATCH_REQUIRE(algorithm != nullptr); + + size_t degree = 0; + bool success = svs_algorithm_vamana_get_graph_degree(algorithm, °ree, nullptr); + CATCH_REQUIRE(success == true); + CATCH_REQUIRE(degree == 64); + + svs_algorithm_free(algorithm); + } + + CATCH_SECTION("Vamana Algorithm Invalid Parameters") { + svs_error_h error = svs_error_create(); + + // Try to create with invalid parameters + svs_algorithm_h algorithm = svs_algorithm_create_vamana(0, 0, 0, error); + CATCH_REQUIRE(algorithm == nullptr); + CATCH_REQUIRE(svs_error_ok(error) == false); + + svs_error_free(error); + } +} diff --git a/bindings/c/tests/c_api_dynamic_index.cpp b/bindings/c/tests/c_api_dynamic_index.cpp new file mode 100644 index 00000000..e629bbf2 --- /dev/null +++ b/bindings/c/tests/c_api_dynamic_index.cpp @@ -0,0 +1,419 @@ +/* + * Copyright 2026 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// C API +#include "svs/c_api/svs_c.h" + +// catch2 +#include "catch2/catch_test_macros.hpp" + +// Standard library +#include +#include + +namespace { + +// Helper function to generate test data +void generate_test_data(std::vector& data, size_t num_vectors, size_t dimension) { + data.resize(num_vectors * dimension); + for (size_t i = 0; i < data.size(); ++i) { + data[i] = static_cast((i * 13) % 100) / 100.0f; + } +} + +// Sequential threadpool for testing +size_t sequential_tp_size(void* /*self*/) { return 1; } + +void sequential_tp_parallel_for( + void* /*self*/, void (*func)(void*, size_t), void* svs_param, size_t n +) { + for (size_t i = 0; i < n; ++i) { + func(svs_param, i); + } +} + +} // namespace + +CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { + const size_t NUM_VECTORS = 50; + const size_t DIMENSION = 32; + const size_t K = 5; + + std::vector data; + std::vector ids(NUM_VECTORS); + generate_test_data(data, NUM_VECTORS, DIMENSION); + + // Generate sequential IDs + for (size_t i = 0; i < NUM_VECTORS; ++i) { + ids[i] = i; + } + + CATCH_SECTION("Dynamic Index Build with IDs") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + CATCH_REQUIRE(algorithm != nullptr); + + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + CATCH_REQUIRE(builder != nullptr); + + // Build dynamic index with explicit IDs + svs_index_h index = svs_index_build_dynamic( + builder, data.data(), ids.data(), NUM_VECTORS, 0, error + ); + CATCH_REQUIRE(index != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Dynamic Index Build without IDs") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + CATCH_REQUIRE(algorithm != nullptr); + + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + CATCH_REQUIRE(builder != nullptr); + + // Build dynamic index without explicit IDs (auto-generated) + svs_index_h index = + svs_index_build_dynamic(builder, data.data(), nullptr, NUM_VECTORS, 0, error); + CATCH_REQUIRE(index != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Dynamic Index Has ID") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + + svs_index_h index = svs_index_build_dynamic( + builder, data.data(), ids.data(), NUM_VECTORS, 0, error + ); + CATCH_REQUIRE(index != nullptr); + + // Check for existing IDs + for (size_t i = 0; i < 5; ++i) { + bool has_id = false; + bool success = svs_index_dynamic_has_id(index, ids[i], &has_id, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(has_id == true); + } + + // Check for non-existing ID + bool has_id = false; + bool success = svs_index_dynamic_has_id(index, NUM_VECTORS + 100, &has_id, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(has_id == false); + + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Dynamic Index Add Points") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + + svs_index_h index = svs_index_build_dynamic( + builder, data.data(), ids.data(), NUM_VECTORS, 0, error + ); + CATCH_REQUIRE(index != nullptr); + + // Add new points + size_t num_new_points = 5; + std::vector new_data; + std::vector new_ids(num_new_points); + generate_test_data(new_data, num_new_points, DIMENSION); + + for (size_t i = 0; i < num_new_points; ++i) { + new_ids[i] = NUM_VECTORS + i; + } + + size_t added_count = svs_index_dynamic_add_points( + index, new_data.data(), new_ids.data(), num_new_points, error + ); + CATCH_REQUIRE(added_count == num_new_points); + CATCH_REQUIRE(svs_error_ok(error)); + + // Verify new IDs exist + for (size_t i = 0; i < num_new_points; ++i) { + bool has_id = false; + bool success = svs_index_dynamic_has_id(index, new_ids[i], &has_id, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(has_id == true); + } + + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Dynamic Index Delete Points") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + + svs_index_h index = svs_index_build_dynamic( + builder, data.data(), ids.data(), NUM_VECTORS, 0, error + ); + CATCH_REQUIRE(index != nullptr); + + // Delete some points + size_t ids_to_delete[] = {0, 5, 10}; + size_t num_to_delete = 3; + + size_t deleted_count = + svs_index_dynamic_delete_points(index, ids_to_delete, num_to_delete, error); + CATCH_REQUIRE(deleted_count == num_to_delete); + CATCH_REQUIRE(svs_error_ok(error)); + + // Verify deleted IDs don't exist + for (size_t i = 0; i < num_to_delete; ++i) { + bool has_id = false; + bool success = + svs_index_dynamic_has_id(index, ids_to_delete[i], &has_id, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(has_id == false); + } + + // Verify other IDs still exist + bool has_id = false; + bool success = svs_index_dynamic_has_id(index, 1, &has_id, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(has_id == true); + + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Dynamic Index Add and Delete") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + + svs_index_h index = svs_index_build_dynamic( + builder, data.data(), ids.data(), NUM_VECTORS, 0, error + ); + CATCH_REQUIRE(index != nullptr); + + // Delete some points + size_t ids_to_delete[] = {0, 1}; + svs_index_dynamic_delete_points(index, ids_to_delete, 2, error); + CATCH_REQUIRE(svs_error_ok(error)); + + // Add new points with the deleted IDs + std::vector new_data; + generate_test_data(new_data, 2, DIMENSION); + + size_t added_count = + svs_index_dynamic_add_points(index, new_data.data(), ids_to_delete, 2, error); + CATCH_REQUIRE(added_count == 2); + CATCH_REQUIRE(svs_error_ok(error)); + + // Verify IDs exist again + for (size_t i = 0; i < 2; ++i) { + bool has_id = false; + bool success = + svs_index_dynamic_has_id(index, ids_to_delete[i], &has_id, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(has_id == true); + } + + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Dynamic Index Consolidate") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + + svs_index_h index = svs_index_build_dynamic( + builder, data.data(), ids.data(), NUM_VECTORS, 0, error + ); + CATCH_REQUIRE(index != nullptr); + + // Add and delete some points + std::vector new_data; + std::vector new_ids = {NUM_VECTORS, NUM_VECTORS + 1}; + generate_test_data(new_data, 2, DIMENSION); + + svs_index_dynamic_add_points(index, new_data.data(), new_ids.data(), 2, error); + + size_t ids_to_delete[] = {0, 1}; + svs_index_dynamic_delete_points(index, ids_to_delete, 2, error); + + // Consolidate the index + bool success = svs_index_dynamic_consolidate(index, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Dynamic Index Compact") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + + svs_index_h index = svs_index_build_dynamic( + builder, data.data(), ids.data(), NUM_VECTORS, 0, error + ); + CATCH_REQUIRE(index != nullptr); + + // Compact the index + bool success = svs_index_dynamic_compact(index, 0, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + + // Delete some points + size_t ids_to_delete[] = {0, 1, 2}; + svs_index_dynamic_delete_points(index, ids_to_delete, 3, error); + CATCH_REQUIRE(svs_error_ok(error)); + + // Consolidate the index + success = svs_index_dynamic_consolidate(index, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + + // Compact the index + success = svs_index_dynamic_compact(index, 0, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Dynamic Index Search After Modifications") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + + svs_index_h index = svs_index_build_dynamic( + builder, data.data(), ids.data(), NUM_VECTORS, 0, error + ); + CATCH_REQUIRE(index != nullptr); + + // Add some points + std::vector new_data; + std::vector new_ids = {NUM_VECTORS, NUM_VECTORS + 1, NUM_VECTORS + 2}; + generate_test_data(new_data, 3, DIMENSION); + svs_index_dynamic_add_points(index, new_data.data(), new_ids.data(), 3, error); + + // Delete some points + size_t ids_to_delete[] = {0, 1}; + svs_index_dynamic_delete_points(index, ids_to_delete, 2, error); + + // Perform search + std::vector queries; + generate_test_data(queries, 2, DIMENSION); + + svs_search_results_t results = + svs_index_search(index, queries.data(), 2, K, nullptr, error); + CATCH_REQUIRE(results != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(results->num_queries == 2); + + // Verify deleted IDs don't appear in results + for (size_t i = 0; i < results->num_queries * K; ++i) { + size_t result_id = results->indices[i]; + CATCH_REQUIRE(result_id != 0); + CATCH_REQUIRE(result_id != 1); + } + + svs_search_results_free(results); + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Dynamic Index Delete Non-existing ID") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + + svs_index_h index = svs_index_build_dynamic( + builder, data.data(), ids.data(), NUM_VECTORS, 0, error + ); + CATCH_REQUIRE(index != nullptr); + + // Try to delete non-existing ID + size_t non_existing_id = NUM_VECTORS + 1000; + size_t deleted_count = + svs_index_dynamic_delete_points(index, &non_existing_id, 1, error); + // Should return 0 for non-existing ID + CATCH_REQUIRE(deleted_count == 0); + + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } +} diff --git a/bindings/c/tests/c_api_error.cpp b/bindings/c/tests/c_api_error.cpp new file mode 100644 index 00000000..e62c8888 --- /dev/null +++ b/bindings/c/tests/c_api_error.cpp @@ -0,0 +1,79 @@ +/* + * Copyright 2026 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// C API +#include "svs/c_api/svs_c.h" + +// catch2 +#include "catch2/catch_test_macros.hpp" + +CATCH_TEST_CASE("C API Error Handling", "[c_api][error]") { + CATCH_SECTION("Error Creation and Cleanup") { + svs_error_h error = svs_error_create(); + CATCH_REQUIRE(error != nullptr); + CATCH_REQUIRE(svs_error_ok(error) == true); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_OK); + CATCH_REQUIRE(svs_error_get_message(error) != nullptr); + svs_error_free(error); + } + + CATCH_SECTION("Error State After API Call") { + svs_error_h error = svs_error_create(); + CATCH_REQUIRE(error != nullptr); + + // Create a valid algorithm - should not set error + svs_algorithm_h algorithm = svs_algorithm_create_vamana(64, 128, 100, error); + CATCH_REQUIRE(algorithm != nullptr); + CATCH_REQUIRE(svs_error_ok(error) == true); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_OK); + + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Error State After Invalid API Call") { + svs_error_h error = svs_error_create(); + CATCH_REQUIRE(error != nullptr); + + // Try to create algorithm with invalid parameters (e.g., 0 graph degree) + svs_algorithm_h algorithm = svs_algorithm_create_vamana(0, 0, 0, error); + CATCH_REQUIRE(algorithm == nullptr); + CATCH_REQUIRE(svs_error_ok(error) == false); + CATCH_REQUIRE(svs_error_get_code(error) != SVS_OK); + CATCH_REQUIRE(svs_error_get_message(error) != nullptr); + + svs_error_free(error); + } + + CATCH_SECTION("Multiple Error Handles") { + svs_error_h error1 = svs_error_create(); + svs_error_h error2 = svs_error_create(); + + CATCH_REQUIRE(error1 != nullptr); + CATCH_REQUIRE(error2 != nullptr); + CATCH_REQUIRE(error1 != error2); + + svs_error_free(error1); + svs_error_free(error2); + } + + CATCH_SECTION("NULL Error Handle") { + // API calls should work with NULL error handle + svs_algorithm_h algorithm = svs_algorithm_create_vamana(64, 128, 100, nullptr); + CATCH_REQUIRE(algorithm != nullptr); + svs_algorithm_free(algorithm); + } +} diff --git a/bindings/c/tests/c_api_index.cpp b/bindings/c/tests/c_api_index.cpp new file mode 100644 index 00000000..eed471d8 --- /dev/null +++ b/bindings/c/tests/c_api_index.cpp @@ -0,0 +1,583 @@ +/* + * Copyright 2026 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// C API +#include "svs/c_api/svs_c.h" + +// catch2 +#include "catch2/catch_test_macros.hpp" + +// Standard library +#include +#include +#include + +namespace { + +// Helper function to generate test data +void generate_test_data(std::vector& data, size_t num_vectors, size_t dimension) { + data.resize(num_vectors * dimension); + for (size_t i = 0; i < data.size(); ++i) { + data[i] = static_cast((i * 7) % 100) / 100.0f; + } +} + +// Helper to calculate Euclidean distance +float euclidean_distance(const float* a, const float* b, size_t dim) { + float sum = 0.0f; + for (size_t i = 0; i < dim; ++i) { + float diff = a[i] - b[i]; + sum += diff * diff; + } + return std::sqrt(sum); +} + +// Sequential threadpool for testing +size_t sequential_tp_size(void* /*self*/) { return 1; } + +void sequential_tp_parallel_for( + void* /*self*/, void (*func)(void*, size_t), void* svs_param, size_t n +) { + for (size_t i = 0; i < n; ++i) { + func(svs_param, i); + } +} + +} // namespace + +CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") { + const size_t NUM_VECTORS = 100; + const size_t NUM_QUERIES = 5; + const size_t DIMENSION = 32; + const size_t K = 10; + + std::vector data; + std::vector queries; + generate_test_data(data, NUM_VECTORS, DIMENSION); + generate_test_data(queries, NUM_QUERIES, DIMENSION); + + CATCH_SECTION("Basic Index Build and Search") { + svs_error_h error = svs_error_create(); + + // Create algorithm + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + CATCH_REQUIRE(algorithm != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + // Create builder + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + CATCH_REQUIRE(builder != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + // Build index with default threadpool + svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); + CATCH_REQUIRE(index != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + // Create search parameters + svs_search_params_h search_params = svs_search_params_create_vamana(50, error); + CATCH_REQUIRE(search_params != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + // Perform search + svs_search_results_t results = + svs_index_search(index, queries.data(), NUM_QUERIES, K, search_params, error); + CATCH_REQUIRE(results != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + // Validate results structure + CATCH_REQUIRE(results->num_queries == NUM_QUERIES); + CATCH_REQUIRE(results->results_per_query != nullptr); + CATCH_REQUIRE(results->indices != nullptr); + CATCH_REQUIRE(results->distances != nullptr); + + // Check that each query returned K results + for (size_t i = 0; i < NUM_QUERIES; ++i) { + CATCH_REQUIRE(results->results_per_query[i] == K); + } + + // Check that indices are within valid range + for (size_t i = 0; i < NUM_QUERIES * K; ++i) { + CATCH_REQUIRE(results->indices[i] < NUM_VECTORS); + } + + // Check that distances are non-negative + for (size_t i = 0; i < NUM_QUERIES * K; ++i) { + CATCH_REQUIRE(results->distances[i] >= 0.0f); + } + + // Cleanup + svs_search_results_free(results); + svs_search_params_free(search_params); + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Index Search without Search Parameters") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + CATCH_REQUIRE(algorithm != nullptr); + + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + CATCH_REQUIRE(builder != nullptr); + + svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); + CATCH_REQUIRE(index != nullptr); + + // Search without explicit search parameters (uses defaults) + svs_search_results_t results = + svs_index_search(index, queries.data(), NUM_QUERIES, K, nullptr, error); + CATCH_REQUIRE(results != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(results->num_queries == NUM_QUERIES); + + svs_search_results_free(results); + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Index with Different Storage Types") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + CATCH_REQUIRE(algorithm != nullptr); + + // Test with Float16 storage + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + CATCH_REQUIRE(builder != nullptr); + + svs_storage_h storage = svs_storage_create_simple(SVS_DATA_TYPE_FLOAT16, error); + CATCH_REQUIRE(storage != nullptr); + + bool success = svs_index_builder_set_storage(builder, storage, error); + CATCH_REQUIRE(success); + + svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); + CATCH_REQUIRE(index != nullptr); + + svs_search_results_t results = + svs_index_search(index, queries.data(), NUM_QUERIES, K, nullptr, error); + CATCH_REQUIRE(results != nullptr); + CATCH_REQUIRE(results->num_queries == NUM_QUERIES); + + svs_search_results_free(results); + svs_index_free(index); + svs_storage_free(storage); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Index with Custom Threadpool") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + + // Set custom threadpool + struct svs_threadpool_interface custom_pool = { + {sequential_tp_size, sequential_tp_parallel_for}, nullptr}; + bool success = + svs_index_builder_set_threadpool_custom(builder, &custom_pool, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + + svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); + CATCH_REQUIRE(index != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + // Verify index works with custom threadpool + svs_search_results_t results = + svs_index_search(index, queries.data(), NUM_QUERIES, K, nullptr, error); + CATCH_REQUIRE(results != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + svs_search_results_free(results); + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Index Get Distance") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + + svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); + CATCH_REQUIRE(index != nullptr); + + // Get distance from first vector to first query + float distance = -1.0f; + bool success = svs_index_get_distance(index, 0, queries.data(), &distance, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(distance >= 0.0f); + + // Verify distance is approximately correct + float expected_distance = + euclidean_distance(data.data(), queries.data(), DIMENSION); + CATCH_REQUIRE(std::abs(distance - expected_distance) < 0.1f); + + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Index Reconstruct") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + + svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); + CATCH_REQUIRE(index != nullptr); + + // Reconstruct first 3 vectors + size_t ids[] = {0, 5, 10}; + size_t num_ids = 3; + std::vector reconstructed(num_ids * DIMENSION); + + bool success = svs_index_reconstruct( + index, ids, num_ids, reconstructed.data(), DIMENSION, error + ); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + + // Verify reconstructed data is close to original + for (size_t i = 0; i < num_ids; ++i) { + size_t id = ids[i]; + const float* original = &data[id * DIMENSION]; + const float* recon = &reconstructed[i * DIMENSION]; + + float distance = euclidean_distance(original, recon, DIMENSION); + CATCH_REQUIRE(distance < 1.0f); // Allow some reconstruction error + } + + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Index Search with Different K Values") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + + svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); + CATCH_REQUIRE(index != nullptr); + + // Test with different K values + size_t k_values[] = {1, 5, 10, 20}; + for (size_t i = 0; i < sizeof(k_values) / sizeof(k_values[0]); ++i) { + size_t k = k_values[i]; + svs_search_results_t results = + svs_index_search(index, queries.data(), NUM_QUERIES, k, nullptr, error); + CATCH_REQUIRE(results != nullptr); + CATCH_REQUIRE(results->num_queries == NUM_QUERIES); + + for (size_t q = 0; q < NUM_QUERIES; ++q) { + CATCH_REQUIRE(results->results_per_query[q] == k); + } + + svs_search_results_free(results); + } + + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Multiple Searches on Same Index") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + + svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); + CATCH_REQUIRE(index != nullptr); + + // Perform multiple searches + for (size_t i = 0; i < 3; ++i) { + svs_search_results_t results = + svs_index_search(index, queries.data(), NUM_QUERIES, K, nullptr, error); + CATCH_REQUIRE(results != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(results->num_queries == NUM_QUERIES); + svs_search_results_free(results); + } + + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } +} + +CATCH_TEST_CASE("C API Threadpool Management", "[c_api][index][threadpool]") { + const size_t NUM_VECTORS = 100; + const size_t DIMENSION = 32; + + std::vector data; + generate_test_data(data, NUM_VECTORS, DIMENSION); + + CATCH_SECTION("Native Threadpool Get/Set Num Threads") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + + // Set native threadpool + bool success = + svs_index_builder_set_threadpool(builder, SVS_THREADPOOL_KIND_NATIVE, 2, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + + svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); + CATCH_REQUIRE(index != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + // Get current number of threads + size_t num_threads = 0; + success = svs_index_get_num_threads(index, &num_threads, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(num_threads == 2); + + // Set to different number of threads + success = svs_index_set_num_threads(index, 4, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + + // Verify the change + success = svs_index_get_num_threads(index, &num_threads, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(num_threads == 4); + + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("OMP Threadpool Get/Set Num Threads") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + + // Set OMP threadpool + bool success = + svs_index_builder_set_threadpool(builder, SVS_THREADPOOL_KIND_OMP, 3, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + + svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); + CATCH_REQUIRE(index != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + // Get current number of threads + size_t num_threads = 0; + success = svs_index_get_num_threads(index, &num_threads, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(num_threads == 3); + + // Set to different number of threads + success = svs_index_set_num_threads(index, 5, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + + // Verify the change + success = svs_index_get_num_threads(index, &num_threads, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(num_threads == 5); + + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Custom Threadpool Get/Set Num Threads") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + + // Set custom threadpool + struct svs_threadpool_interface custom_pool = { + {sequential_tp_size, sequential_tp_parallel_for}, nullptr}; + bool success = + svs_index_builder_set_threadpool_custom(builder, &custom_pool, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + + svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); + CATCH_REQUIRE(index != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + // Get number of threads from custom threadpool + size_t num_threads = 0; + success = svs_index_get_num_threads(index, &num_threads, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(num_threads == 1); // Sequential threadpool reports size 1 + + // Setting num_threads on custom threadpool should fail with + // SVS_ERROR_INVALID_OPERATION + success = svs_index_set_num_threads(index, 2, error); + CATCH_REQUIRE_FALSE(success); + CATCH_REQUIRE_FALSE(svs_error_ok(error)); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_OPERATION); + + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Single Thread Threadpool") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + + // Set single thread threadpool + bool success = svs_index_builder_set_threadpool( + builder, SVS_THREADPOOL_KIND_SINGLE_THREAD, 1, error + ); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + + svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); + CATCH_REQUIRE(index != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + // Get number of threads + size_t num_threads = 0; + success = svs_index_get_num_threads(index, &num_threads, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(num_threads == 1); + + // Try to set number of threads (should fail with SVS_ERROR_INVALID_OPERATION since + // it's single thread) + success = svs_index_set_num_threads(index, 2, error); + CATCH_REQUIRE_FALSE(success); + CATCH_REQUIRE_FALSE(svs_error_ok(error)); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_OPERATION); + + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Default Threadpool") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + + // Don't set any threadpool - use default + svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); + CATCH_REQUIRE(index != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + // Get number of threads from default threadpool + size_t num_threads = 0; + bool success = svs_index_get_num_threads(index, &num_threads, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(num_threads > 0); // Should have at least 1 thread + + // Try to set number of threads + success = svs_index_set_num_threads(index, 2, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + + // Verify the change + success = svs_index_get_num_threads(index, &num_threads, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(num_threads == 2); + CATCH_REQUIRE(svs_error_ok(error)); + + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Invalid Set Num Threads") { + svs_error_h error = svs_error_create(); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + + svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); + CATCH_REQUIRE(index != nullptr); + + // Try to set to 0 threads (invalid) - should fail with SVS_ERROR_INVALID_ARGUMENT + bool success = svs_index_set_num_threads(index, 0, error); + CATCH_REQUIRE(success == false); + CATCH_REQUIRE(svs_error_ok(error) == false); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); + + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } +} diff --git a/bindings/c/tests/c_api_index_builder.cpp b/bindings/c/tests/c_api_index_builder.cpp new file mode 100644 index 00000000..5aaa8d13 --- /dev/null +++ b/bindings/c/tests/c_api_index_builder.cpp @@ -0,0 +1,226 @@ +/* + * Copyright 2026 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// C API +#include "svs/c_api/svs_c.h" + +// catch2 +#include "catch2/catch_test_macros.hpp" + +// Standard library +#include + +namespace { + +// Helper function to generate random test data +void generate_test_data(std::vector& data, size_t num_vectors, size_t dimension) { + data.resize(num_vectors * dimension); + for (size_t i = 0; i < data.size(); ++i) { + data[i] = static_cast(i % 100) / 100.0f; + } +} + +// Sequential threadpool implementation for testing +size_t sequential_tp_size(void* /*self*/) { return 1; } + +void sequential_tp_parallel_for( + void* /*self*/, void (*func)(void*, size_t), void* svs_param, size_t n +) { + for (size_t i = 0; i < n; ++i) { + func(svs_param, i); + } +} + +} // namespace + +CATCH_TEST_CASE("C API Index Builder", "[c_api][index_builder]") { + CATCH_SECTION("Index Builder Creation") { + svs_error_h error = svs_error_create(); + svs_algorithm_h algorithm = svs_algorithm_create_vamana(64, 128, 100, error); + CATCH_REQUIRE(algorithm != nullptr); + + svs_index_builder_h builder = + svs_index_builder_create(SVS_DISTANCE_METRIC_EUCLIDEAN, 128, algorithm, error); + CATCH_REQUIRE(builder != nullptr); + CATCH_REQUIRE(svs_error_ok(error) == true); + + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Index Builder with Different Metrics") { + svs_error_h error = svs_error_create(); + svs_algorithm_h algorithm = svs_algorithm_create_vamana(64, 128, 100, error); + CATCH_REQUIRE(algorithm != nullptr); + + // Euclidean + svs_index_builder_h builder1 = + svs_index_builder_create(SVS_DISTANCE_METRIC_EUCLIDEAN, 128, algorithm, error); + CATCH_REQUIRE(builder1 != nullptr); + CATCH_REQUIRE(svs_error_ok(error) == true); + + // Cosine + svs_index_builder_h builder2 = + svs_index_builder_create(SVS_DISTANCE_METRIC_COSINE, 128, algorithm, error); + CATCH_REQUIRE(builder2 != nullptr); + CATCH_REQUIRE(svs_error_ok(error) == true); + + // Dot Product + svs_index_builder_h builder3 = svs_index_builder_create( + SVS_DISTANCE_METRIC_DOT_PRODUCT, 128, algorithm, error + ); + CATCH_REQUIRE(builder3 != nullptr); + CATCH_REQUIRE(svs_error_ok(error) == true); + + svs_index_builder_free(builder1); + svs_index_builder_free(builder2); + svs_index_builder_free(builder3); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Index Builder Set Storage") { + svs_error_h error = svs_error_create(); + svs_algorithm_h algorithm = svs_algorithm_create_vamana(64, 128, 100, error); + CATCH_REQUIRE(algorithm != nullptr); + + svs_index_builder_h builder = + svs_index_builder_create(SVS_DISTANCE_METRIC_EUCLIDEAN, 128, algorithm, error); + CATCH_REQUIRE(builder != nullptr); + + svs_storage_h storage = svs_storage_create_simple(SVS_DATA_TYPE_FLOAT32, error); + CATCH_REQUIRE(storage != nullptr); + + bool success = svs_index_builder_set_storage(builder, storage, error); + CATCH_REQUIRE(success == true); + CATCH_REQUIRE(svs_error_ok(error) == true); + + svs_index_builder_free(builder); + svs_storage_free(storage); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Index Builder Set Threadpool Native") { + svs_error_h error = svs_error_create(); + svs_algorithm_h algorithm = svs_algorithm_create_vamana(64, 128, 100, error); + CATCH_REQUIRE(algorithm != nullptr); + + svs_index_builder_h builder = + svs_index_builder_create(SVS_DISTANCE_METRIC_EUCLIDEAN, 128, algorithm, error); + CATCH_REQUIRE(builder != nullptr); + + bool success = + svs_index_builder_set_threadpool(builder, SVS_THREADPOOL_KIND_NATIVE, 2, error); + CATCH_REQUIRE(success == true); + CATCH_REQUIRE(svs_error_ok(error) == true); + + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Index Builder Set Threadpool OMP") { + svs_error_h error = svs_error_create(); + svs_algorithm_h algorithm = svs_algorithm_create_vamana(64, 128, 100, error); + CATCH_REQUIRE(algorithm != nullptr); + + svs_index_builder_h builder = + svs_index_builder_create(SVS_DISTANCE_METRIC_EUCLIDEAN, 128, algorithm, error); + CATCH_REQUIRE(builder != nullptr); + + bool success = + svs_index_builder_set_threadpool(builder, SVS_THREADPOOL_KIND_OMP, 2, error); + CATCH_REQUIRE(success == true); + CATCH_REQUIRE(svs_error_ok(error) == true); + + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Index Builder Set Custom Threadpool") { + svs_error_h error = svs_error_create(); + svs_algorithm_h algorithm = svs_algorithm_create_vamana(64, 128, 100, error); + CATCH_REQUIRE(algorithm != nullptr); + + svs_index_builder_h builder = + svs_index_builder_create(SVS_DISTANCE_METRIC_EUCLIDEAN, 128, algorithm, error); + CATCH_REQUIRE(builder != nullptr); + + struct svs_threadpool_interface custom_pool = { + {sequential_tp_size, sequential_tp_parallel_for}, nullptr}; + + bool success = + svs_index_builder_set_threadpool_custom(builder, &custom_pool, error); + CATCH_REQUIRE(success == true); + CATCH_REQUIRE(svs_error_ok(error) == true); + + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Index Builder with NULL Error") { + svs_algorithm_h algorithm = svs_algorithm_create_vamana(64, 128, 100, nullptr); + CATCH_REQUIRE(algorithm != nullptr); + + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, 128, algorithm, nullptr + ); + CATCH_REQUIRE(builder != nullptr); + + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + } + + CATCH_SECTION("Index Builder with Various Dimensions") { + svs_error_h error = svs_error_create(); + svs_algorithm_h algorithm = svs_algorithm_create_vamana(64, 128, 100, error); + CATCH_REQUIRE(algorithm != nullptr); + + size_t dimensions[] = {32, 64, 128, 256, 384, 512, 768, 1024}; + for (size_t i = 0; i < sizeof(dimensions) / sizeof(dimensions[0]); ++i) { + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, dimensions[i], algorithm, error + ); + CATCH_REQUIRE(builder != nullptr); + CATCH_REQUIRE(svs_error_ok(error) == true); + svs_index_builder_free(builder); + } + + svs_algorithm_free(algorithm); + svs_error_free(error); + } + + CATCH_SECTION("Index Builder Invalid Parameters") { + svs_error_h error = svs_error_create(); + svs_algorithm_h algorithm = svs_algorithm_create_vamana(64, 128, 100, error); + CATCH_REQUIRE(algorithm != nullptr); + + // Try to create with 0 dimension + svs_index_builder_h builder = + svs_index_builder_create(SVS_DISTANCE_METRIC_EUCLIDEAN, 0, algorithm, error); + // Behavior depends on implementation + if (builder != nullptr) { + svs_index_builder_free(builder); + } + + svs_algorithm_free(algorithm); + svs_error_free(error); + } +} diff --git a/bindings/c/tests/c_api_search_params.cpp b/bindings/c/tests/c_api_search_params.cpp new file mode 100644 index 00000000..0ea7130c --- /dev/null +++ b/bindings/c/tests/c_api_search_params.cpp @@ -0,0 +1,88 @@ +/* + * Copyright 2026 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// C API +#include "svs/c_api/svs_c.h" + +// catch2 +#include "catch2/catch_test_macros.hpp" + +CATCH_TEST_CASE("C API Search Parameters", "[c_api][search_params]") { + CATCH_SECTION("Vamana Search Parameters Creation") { + svs_error_h error = svs_error_create(); + + size_t search_window_size = 100; + svs_search_params_h params = + svs_search_params_create_vamana(search_window_size, error); + CATCH_REQUIRE(params != nullptr); + CATCH_REQUIRE(svs_error_ok(error) == true); + + svs_search_params_free(params); + svs_error_free(error); + } + + CATCH_SECTION("Vamana Search Parameters Various Sizes") { + svs_error_h error = svs_error_create(); + + size_t sizes[] = {10, 50, 100, 200, 500, 1000}; + for (size_t i = 0; i < sizeof(sizes) / sizeof(sizes[0]); ++i) { + svs_search_params_h params = svs_search_params_create_vamana(sizes[i], error); + CATCH_REQUIRE(params != nullptr); + CATCH_REQUIRE(svs_error_ok(error) == true); + svs_search_params_free(params); + } + + svs_error_free(error); + } + + CATCH_SECTION("Search Parameters with NULL Error") { + svs_search_params_h params = svs_search_params_create_vamana(100, nullptr); + CATCH_REQUIRE(params != nullptr); + + svs_search_params_free(params); + } + + CATCH_SECTION("Multiple Search Parameters Handles") { + svs_error_h error = svs_error_create(); + + svs_search_params_h params1 = svs_search_params_create_vamana(50, error); + svs_search_params_h params2 = svs_search_params_create_vamana(100, error); + svs_search_params_h params3 = svs_search_params_create_vamana(200, error); + + CATCH_REQUIRE(params1 != nullptr); + CATCH_REQUIRE(params2 != nullptr); + CATCH_REQUIRE(params3 != nullptr); + CATCH_REQUIRE(svs_error_ok(error) == true); + + svs_search_params_free(params1); + svs_search_params_free(params2); + svs_search_params_free(params3); + svs_error_free(error); + } + + CATCH_SECTION("Search Parameters with Invalid Size") { + svs_error_h error = svs_error_create(); + + // Try to create with size 0 + svs_search_params_h params = svs_search_params_create_vamana(0, error); + // Behavior depends on implementation - either nullptr or valid handle + if (params != nullptr) { + svs_search_params_free(params); + } + + svs_error_free(error); + } +} diff --git a/bindings/c/tests/c_api_storage.cpp b/bindings/c/tests/c_api_storage.cpp new file mode 100644 index 00000000..b11f4d72 --- /dev/null +++ b/bindings/c/tests/c_api_storage.cpp @@ -0,0 +1,181 @@ +/* + * Copyright 2026 Intel Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// C API +#include "svs/c_api/svs_c.h" + +// catch2 +#include "catch2/catch_test_macros.hpp" + +CATCH_TEST_CASE("C API Storage", "[c_api][storage]") { + CATCH_SECTION("Simple Storage Float32") { + svs_error_h error = svs_error_create(); + + svs_storage_h storage = svs_storage_create_simple(SVS_DATA_TYPE_FLOAT32, error); + CATCH_REQUIRE(storage != nullptr); + CATCH_REQUIRE(svs_error_ok(error) == true); + + svs_storage_free(storage); + svs_error_free(error); + } + + CATCH_SECTION("Simple Storage Float16") { + svs_error_h error = svs_error_create(); + + svs_storage_h storage = svs_storage_create_simple(SVS_DATA_TYPE_FLOAT16, error); + CATCH_REQUIRE(storage != nullptr); + CATCH_REQUIRE(svs_error_ok(error) == true); + + svs_storage_free(storage); + svs_error_free(error); + } + + CATCH_SECTION("Simple Storage INT8") { + svs_error_h error = svs_error_create(); + + svs_storage_h storage = svs_storage_create_simple(SVS_DATA_TYPE_INT8, error); + CATCH_REQUIRE(storage == nullptr); + CATCH_REQUIRE_FALSE(svs_error_ok(error)); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); + + svs_storage_free(storage); + svs_error_free(error); + } + + CATCH_SECTION("Simple Storage UINT8") { + svs_error_h error = svs_error_create(); + + svs_storage_h storage = svs_storage_create_simple(SVS_DATA_TYPE_UINT8, error); + CATCH_REQUIRE(storage == nullptr); + CATCH_REQUIRE_FALSE(svs_error_ok(error)); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); + + svs_storage_free(storage); + svs_error_free(error); + } + + CATCH_SECTION("LeanVec Storage") { + svs_error_h error = svs_error_create(); + + size_t leanvec_dims = 64; + svs_storage_h storage = svs_storage_create_leanvec( + leanvec_dims, SVS_DATA_TYPE_UINT8, SVS_DATA_TYPE_UINT8, error + ); + CATCH_REQUIRE(storage != nullptr); + CATCH_REQUIRE(svs_error_ok(error) == true); + + svs_storage_free(storage); + svs_error_free(error); + } + + CATCH_SECTION("LeanVec Storage UINT4") { + svs_error_h error = svs_error_create(); + + size_t leanvec_dims = 64; + svs_storage_h storage = svs_storage_create_leanvec( + leanvec_dims, SVS_DATA_TYPE_UINT4, SVS_DATA_TYPE_UINT4, error + ); + CATCH_REQUIRE(storage != nullptr); + CATCH_REQUIRE(svs_error_ok(error) == true); + + svs_storage_free(storage); + svs_error_free(error); + } + + CATCH_SECTION("LVQ Storage UINT4") { + svs_error_h error = svs_error_create(); + + svs_storage_h storage = + svs_storage_create_lvq(SVS_DATA_TYPE_UINT4, SVS_DATA_TYPE_VOID, error); + CATCH_REQUIRE(storage != nullptr); + CATCH_REQUIRE(svs_error_ok(error) == true); + + svs_storage_free(storage); + svs_error_free(error); + } + + CATCH_SECTION("LVQ Storage UINT8") { + svs_error_h error = svs_error_create(); + + svs_storage_h storage = + svs_storage_create_lvq(SVS_DATA_TYPE_UINT8, SVS_DATA_TYPE_VOID, error); + CATCH_REQUIRE(storage != nullptr); + CATCH_REQUIRE(svs_error_ok(error) == true); + + svs_storage_free(storage); + svs_error_free(error); + } + + CATCH_SECTION("LVQ Storage with Residual") { + svs_error_h error = svs_error_create(); + + svs_storage_h storage = + svs_storage_create_lvq(SVS_DATA_TYPE_UINT4, SVS_DATA_TYPE_UINT8, error); + CATCH_REQUIRE(storage != nullptr); + CATCH_REQUIRE(svs_error_ok(error) == true); + + svs_storage_free(storage); + svs_error_free(error); + } + + CATCH_SECTION("Scalar Quantization Storage UINT8") { + svs_error_h error = svs_error_create(); + + svs_storage_h storage = svs_storage_create_sq(SVS_DATA_TYPE_UINT8, error); + CATCH_REQUIRE(storage != nullptr); + CATCH_REQUIRE(svs_error_ok(error) == true); + + svs_storage_free(storage); + svs_error_free(error); + } + + CATCH_SECTION("Scalar Quantization Storage INT8") { + svs_error_h error = svs_error_create(); + + svs_storage_h storage = svs_storage_create_sq(SVS_DATA_TYPE_INT8, error); + CATCH_REQUIRE(storage != nullptr); + CATCH_REQUIRE(svs_error_ok(error) == true); + + svs_storage_free(storage); + svs_error_free(error); + } + + CATCH_SECTION("Storage with NULL Error") { + svs_storage_h storage = svs_storage_create_simple(SVS_DATA_TYPE_FLOAT32, nullptr); + CATCH_REQUIRE(storage != nullptr); + + svs_storage_free(storage); + } + + CATCH_SECTION("Multiple Storage Handles") { + svs_error_h error = svs_error_create(); + + svs_storage_h storage1 = svs_storage_create_simple(SVS_DATA_TYPE_FLOAT32, error); + svs_storage_h storage2 = svs_storage_create_simple(SVS_DATA_TYPE_FLOAT16, error); + svs_storage_h storage3 = + svs_storage_create_leanvec(64, SVS_DATA_TYPE_UINT8, SVS_DATA_TYPE_UINT8, error); + + CATCH_REQUIRE(storage1 != nullptr); + CATCH_REQUIRE(storage2 != nullptr); + CATCH_REQUIRE(storage3 != nullptr); + CATCH_REQUIRE(svs_error_ok(error) == true); + + svs_storage_free(storage1); + svs_storage_free(storage2); + svs_storage_free(storage3); + svs_error_free(error); + } +} From b9020f19cb287b341ef1805310e9646b256dc4d2 Mon Sep 17 00:00:00 2001 From: Rafik Saliev Date: Fri, 27 Mar 2026 16:27:50 +0100 Subject: [PATCH 2/7] Refactor CMake configuration for C API tests --- bindings/c/tests/CMakeLists.txt | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/bindings/c/tests/CMakeLists.txt b/bindings/c/tests/CMakeLists.txt index 5085c0ed..8d6c1309 100644 --- a/bindings/c/tests/CMakeLists.txt +++ b/bindings/c/tests/CMakeLists.txt @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +set(TARGET_NAME svs_c_api_test) + # Check if Catch2 is available find_package(Catch2 3 QUIET) @@ -26,12 +28,15 @@ if(NOT Catch2_FOUND) set(CATCH_CONFIG_FAST_COMPILE OFF CACHE BOOL "" FORCE) set(CATCH_CONFIG_PREFIX_ALL ON CACHE BOOL "" FORCE) + set(PRESET_CMAKE_CXX_STANDARD ${CMAKE_CXX_STANDARD}) + set(CMAKE_CXX_STANDARD 20) FetchContent_Declare( Catch2 GIT_REPOSITORY https://github.com/catchorg/Catch2.git GIT_TAG v3.11.0 ) FetchContent_MakeAvailable(Catch2) + set(CMAKE_CXX_STANDARD ${PRESET_CMAKE_CXX_STANDARD}) endif() # Define test sources @@ -46,24 +51,38 @@ set(C_API_TEST_SOURCES ) # Create test executable -add_executable(svs_c_api_tests ${C_API_TEST_SOURCES}) +add_executable(${TARGET_NAME} ${C_API_TEST_SOURCES}) # Link with C API library and Catch2 -target_link_libraries(svs_c_api_tests PRIVATE +target_link_libraries(${TARGET_NAME} PRIVATE svs_c_api Catch2::Catch2WithMain ) # Set C++ standard -target_compile_features(svs_c_api_tests PRIVATE cxx_std_17) +target_compile_features(${TARGET_NAME} PRIVATE cxx_std_20) +set_target_properties(${TARGET_NAME} PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF +) # Include directories -target_include_directories(svs_c_api_tests PRIVATE +target_include_directories(${TARGET_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include ) # Add test to CTest include(CTest) +enable_testing() + +# Add the test to CTest +add_test(NAME ${TARGET_NAME} COMMAND ${TARGET_NAME}) + +# Set test properties +set_tests_properties(${TARGET_NAME} PROPERTIES + LABELS "c_api" +) # Add Catch2 CMake module path if(NOT Catch2_FOUND) @@ -75,12 +94,12 @@ else() endif() include(Catch) -catch_discover_tests(svs_c_api_tests) +catch_discover_tests(${TARGET_NAME}) # Add a custom target to run tests add_custom_target(run_c_api_tests - COMMAND svs_c_api_tests - DEPENDS svs_c_api_tests + COMMAND ${TARGET_NAME} + DEPENDS ${TARGET_NAME} WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} COMMENT "Running C API tests..." ) From 4f918b6cdde44da2fff1709fd7b3e23669e5302d Mon Sep 17 00:00:00 2001 From: Rafik Saliev Date: Wed, 10 Jun 2026 03:53:04 -0700 Subject: [PATCH 3/7] Enhance C API tests with dynamic index functionality and coverage reporting - Refactor CMake configuration to support coverage options for C API tests. - Update test build options to allow dynamic index testing with configurable block sizes. - Introduce a utility for managing temporary directories in tests. - Improve dynamic index tests to include save/load functionality and multi-threading support. - Update README and test commands for consistency with new test target names. --- bindings/c/CMakeLists.txt | 35 ++++- bindings/c/tests/CMakeLists.txt | 6 +- bindings/c/tests/README.md | 18 +-- bindings/c/tests/c_api_dynamic_index.cpp | 187 +++++++++-------------- bindings/c/tests/c_api_index.cpp | 170 ++++++++++++++++++++- bindings/c/tests/c_api_test_utils.h | 68 +++++++++ 6 files changed, 349 insertions(+), 135 deletions(-) create mode 100644 bindings/c/tests/c_api_test_utils.h diff --git a/bindings/c/CMakeLists.txt b/bindings/c/CMakeLists.txt index aa56ddbb..ab4f6b7c 100644 --- a/bindings/c/CMakeLists.txt +++ b/bindings/c/CMakeLists.txt @@ -69,9 +69,6 @@ set_target_properties(${TARGET_NAME} PROPERTIES VERSION ${PROJECT_VERSION} SOVER target_link_libraries(${TARGET_NAME} PRIVATE svs::svs ) -if (SVS_EXPERIMENTAL_LINK_STATIC_MKL) - link_mkl_static(${TARGET_NAME}) -endif() if (SVS_RUNTIME_ENABLE_LVQ_LEANVEC) message(STATUS "Enabling LVQ/LeanVec support in C API") @@ -95,7 +92,9 @@ if (SVS_RUNTIME_ENABLE_LVQ_LEANVEC) svs::svs svs_compile_options ) - link_mkl_static(${TARGET_NAME}) + if(SVS_EXPERIMENTAL_LINK_STATIC_MKL) + link_mkl_static(${TARGET_NAME}) + endif() elseif(TARGET svs::svs) message(FATAL_ERROR "Pre-built LVQ/LeanVec SVS library cannot be used in SVS main build. " @@ -191,8 +190,34 @@ install(FILES ) # Build tests if requested -option(SVS_BUILD_C_API_TESTS "Build C API tests" ON) +if(DEFINED SVS_BUILD_TESTS) + option(SVS_BUILD_C_API_TESTS "Build C API tests" ${SVS_BUILD_TESTS}) +else() + option(SVS_BUILD_C_API_TESTS "Build C API tests" OFF) +endif() + + if(SVS_BUILD_C_API_TESTS) + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + target_compile_options(${TARGET_NAME} PRIVATE --coverage) + target_link_libraries(${TARGET_NAME} PRIVATE --coverage) + # add coverage target + add_custom_target(clean_coverage + COMMAND ${CMAKE_COMMAND} -E echo "Cleaning coverage data..." + COMMAND ${CMAKE_COMMAND} -E chdir ${CMAKE_BINARY_DIR} find . -name "*.gcda" -delete + COMMAND ${CMAKE_COMMAND} -E remove -f ${CMAKE_BINARY_DIR}/coverage.info + COMMENT "Cleaning coverage data..." + ) + add_custom_target(coverage + COMMAND ${CMAKE_COMMAND} -E echo "Generating coverage report..." + COMMAND ${CMAKE_COMMAND} -E env GCOV_PREFIX=${CMAKE_BINARY_DIR}/coverage GCOV_PREFIX_STRIP=1 ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/coverage + COMMAND ${CMAKE_COMMAND} -E chdir ${CMAKE_BINARY_DIR} lcov --capture --directory . --output-file coverage.info + COMMAND ${CMAKE_COMMAND} -E chdir ${CMAKE_BINARY_DIR} lcov --remove coverage.info '/usr/*' --output-file coverage.info + COMMAND ${CMAKE_COMMAND} -E chdir ${CMAKE_BINARY_DIR} lcov --remove coverage.info '*/_deps/*' --output-file coverage.info + COMMAND ${CMAKE_COMMAND} -E chdir ${CMAKE_BINARY_DIR} lcov --list coverage.info + COMMENT "Generating code coverage report..." + ) + endif() add_subdirectory(tests) endif() diff --git a/bindings/c/tests/CMakeLists.txt b/bindings/c/tests/CMakeLists.txt index 8d6c1309..537e3529 100644 --- a/bindings/c/tests/CMakeLists.txt +++ b/bindings/c/tests/CMakeLists.txt @@ -29,11 +29,11 @@ if(NOT Catch2_FOUND) set(CATCH_CONFIG_PREFIX_ALL ON CACHE BOOL "" FORCE) set(PRESET_CMAKE_CXX_STANDARD ${CMAKE_CXX_STANDARD}) - set(CMAKE_CXX_STANDARD 20) + set(CMAKE_CXX_STANDARD ${SVS_CXX_STANDARD}) FetchContent_Declare( Catch2 GIT_REPOSITORY https://github.com/catchorg/Catch2.git - GIT_TAG v3.11.0 + GIT_TAG v3.4.0 ) FetchContent_MakeAvailable(Catch2) set(CMAKE_CXX_STANDARD ${PRESET_CMAKE_CXX_STANDARD}) @@ -97,7 +97,7 @@ include(Catch) catch_discover_tests(${TARGET_NAME}) # Add a custom target to run tests -add_custom_target(run_c_api_tests +add_custom_target(run_c_api_test COMMAND ${TARGET_NAME} DEPENDS ${TARGET_NAME} WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} diff --git a/bindings/c/tests/README.md b/bindings/c/tests/README.md index 0a1cac58..d595c0d2 100644 --- a/bindings/c/tests/README.md +++ b/bindings/c/tests/README.md @@ -23,7 +23,7 @@ The tests are built as part of the C API build process. To build them: ```bash # From the build directory cmake -DSVS_BUILD_C_API_TESTS=ON .. -make svs_c_api_tests +make svs_c_api_test ``` To disable building tests: @@ -37,41 +37,41 @@ cmake -DSVS_BUILD_C_API_TESTS=OFF .. ### Run all tests ```bash -./svs_c_api_tests +./svs_c_api_test ``` ### Run specific test cases ```bash # Run error handling tests only -./svs_c_api_tests "[c_api][error]" +./svs_c_api_test "[c_api][error]" # Run algorithm tests only -./svs_c_api_tests "[c_api][algorithm]" +./svs_c_api_test "[c_api][algorithm]" # Run all index tests -./svs_c_api_tests "[c_api][index]" +./svs_c_api_test "[c_api][index]" # Run dynamic index tests -./svs_c_api_tests "[c_api][dynamic]" +./svs_c_api_test "[c_api][dynamic]" ``` ### Run with verbose output ```bash -./svs_c_api_tests -s +./svs_c_api_test -s ``` ### List all available tests ```bash -./svs_c_api_tests --list-tests +./svs_c_api_test --list-tests ``` ### Run with CTest ```bash -ctest -R svs_c_api_tests +ctest -R svs_c_api_test ``` ## Test Coverage diff --git a/bindings/c/tests/c_api_dynamic_index.cpp b/bindings/c/tests/c_api_dynamic_index.cpp index e629bbf2..abbc4443 100644 --- a/bindings/c/tests/c_api_dynamic_index.cpp +++ b/bindings/c/tests/c_api_dynamic_index.cpp @@ -20,6 +20,9 @@ // catch2 #include "catch2/catch_test_macros.hpp" +// Test utilities +#include "c_api_test_utils.h" + // Standard library #include #include @@ -51,6 +54,7 @@ CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { const size_t NUM_VECTORS = 50; const size_t DIMENSION = 32; const size_t K = 5; + const size_t BLOCK_SIZE = 1024 * 1024; // 1 MB block size for testing std::vector data; std::vector ids(NUM_VECTORS); @@ -61,63 +65,48 @@ CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { ids[i] = i; } - CATCH_SECTION("Dynamic Index Build with IDs") { - svs_error_h error = svs_error_create(); + svs_error_h error = svs_error_create(); - svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); - CATCH_REQUIRE(algorithm != nullptr); + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + CATCH_REQUIRE(algorithm != nullptr); - svs_index_builder_h builder = svs_index_builder_create( - SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error - ); - CATCH_REQUIRE(builder != nullptr); + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + CATCH_REQUIRE(builder != nullptr); + + // Set single thread threadpool for testing + bool success = svs_index_builder_set_threadpool( + builder, SVS_THREADPOOL_KIND_SINGLE_THREAD, 1, error + ); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_SECTION("Dynamic Index Build with IDs") { // Build dynamic index with explicit IDs svs_index_h index = svs_index_build_dynamic( - builder, data.data(), ids.data(), NUM_VECTORS, 0, error + builder, data.data(), ids.data(), NUM_VECTORS, BLOCK_SIZE, error ); CATCH_REQUIRE(index != nullptr); CATCH_REQUIRE(svs_error_ok(error)); svs_index_free(index); - svs_index_builder_free(builder); - svs_algorithm_free(algorithm); - svs_error_free(error); } CATCH_SECTION("Dynamic Index Build without IDs") { - svs_error_h error = svs_error_create(); - - svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); - CATCH_REQUIRE(algorithm != nullptr); - - svs_index_builder_h builder = svs_index_builder_create( - SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error - ); - CATCH_REQUIRE(builder != nullptr); - // Build dynamic index without explicit IDs (auto-generated) - svs_index_h index = - svs_index_build_dynamic(builder, data.data(), nullptr, NUM_VECTORS, 0, error); + svs_index_h index = svs_index_build_dynamic( + builder, data.data(), nullptr, NUM_VECTORS, BLOCK_SIZE, error + ); CATCH_REQUIRE(index != nullptr); CATCH_REQUIRE(svs_error_ok(error)); svs_index_free(index); - svs_index_builder_free(builder); - svs_algorithm_free(algorithm); - svs_error_free(error); } CATCH_SECTION("Dynamic Index Has ID") { - svs_error_h error = svs_error_create(); - - svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); - svs_index_builder_h builder = svs_index_builder_create( - SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error - ); - svs_index_h index = svs_index_build_dynamic( - builder, data.data(), ids.data(), NUM_VECTORS, 0, error + builder, data.data(), ids.data(), NUM_VECTORS, BLOCK_SIZE, error ); CATCH_REQUIRE(index != nullptr); @@ -138,21 +127,11 @@ CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { CATCH_REQUIRE(has_id == false); svs_index_free(index); - svs_index_builder_free(builder); - svs_algorithm_free(algorithm); - svs_error_free(error); } CATCH_SECTION("Dynamic Index Add Points") { - svs_error_h error = svs_error_create(); - - svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); - svs_index_builder_h builder = svs_index_builder_create( - SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error - ); - svs_index_h index = svs_index_build_dynamic( - builder, data.data(), ids.data(), NUM_VECTORS, 0, error + builder, data.data(), ids.data(), NUM_VECTORS, BLOCK_SIZE, error ); CATCH_REQUIRE(index != nullptr); @@ -181,21 +160,11 @@ CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { } svs_index_free(index); - svs_index_builder_free(builder); - svs_algorithm_free(algorithm); - svs_error_free(error); } CATCH_SECTION("Dynamic Index Delete Points") { - svs_error_h error = svs_error_create(); - - svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); - svs_index_builder_h builder = svs_index_builder_create( - SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error - ); - svs_index_h index = svs_index_build_dynamic( - builder, data.data(), ids.data(), NUM_VECTORS, 0, error + builder, data.data(), ids.data(), NUM_VECTORS, BLOCK_SIZE, error ); CATCH_REQUIRE(index != nullptr); @@ -224,21 +193,11 @@ CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { CATCH_REQUIRE(has_id == true); svs_index_free(index); - svs_index_builder_free(builder); - svs_algorithm_free(algorithm); - svs_error_free(error); } CATCH_SECTION("Dynamic Index Add and Delete") { - svs_error_h error = svs_error_create(); - - svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); - svs_index_builder_h builder = svs_index_builder_create( - SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error - ); - svs_index_h index = svs_index_build_dynamic( - builder, data.data(), ids.data(), NUM_VECTORS, 0, error + builder, data.data(), ids.data(), NUM_VECTORS, BLOCK_SIZE, error ); CATCH_REQUIRE(index != nullptr); @@ -266,21 +225,11 @@ CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { } svs_index_free(index); - svs_index_builder_free(builder); - svs_algorithm_free(algorithm); - svs_error_free(error); } CATCH_SECTION("Dynamic Index Consolidate") { - svs_error_h error = svs_error_create(); - - svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); - svs_index_builder_h builder = svs_index_builder_create( - SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error - ); - svs_index_h index = svs_index_build_dynamic( - builder, data.data(), ids.data(), NUM_VECTORS, 0, error + builder, data.data(), ids.data(), NUM_VECTORS, BLOCK_SIZE, error ); CATCH_REQUIRE(index != nullptr); @@ -300,21 +249,11 @@ CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { CATCH_REQUIRE(svs_error_ok(error)); svs_index_free(index); - svs_index_builder_free(builder); - svs_algorithm_free(algorithm); - svs_error_free(error); } CATCH_SECTION("Dynamic Index Compact") { - svs_error_h error = svs_error_create(); - - svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); - svs_index_builder_h builder = svs_index_builder_create( - SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error - ); - svs_index_h index = svs_index_build_dynamic( - builder, data.data(), ids.data(), NUM_VECTORS, 0, error + builder, data.data(), ids.data(), NUM_VECTORS, BLOCK_SIZE, error ); CATCH_REQUIRE(index != nullptr); @@ -339,21 +278,11 @@ CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { CATCH_REQUIRE(svs_error_ok(error)); svs_index_free(index); - svs_index_builder_free(builder); - svs_algorithm_free(algorithm); - svs_error_free(error); } CATCH_SECTION("Dynamic Index Search After Modifications") { - svs_error_h error = svs_error_create(); - - svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); - svs_index_builder_h builder = svs_index_builder_create( - SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error - ); - svs_index_h index = svs_index_build_dynamic( - builder, data.data(), ids.data(), NUM_VECTORS, 0, error + builder, data.data(), ids.data(), NUM_VECTORS, BLOCK_SIZE, error ); CATCH_REQUIRE(index != nullptr); @@ -386,21 +315,11 @@ CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { svs_search_results_free(results); svs_index_free(index); - svs_index_builder_free(builder); - svs_algorithm_free(algorithm); - svs_error_free(error); } CATCH_SECTION("Dynamic Index Delete Non-existing ID") { - svs_error_h error = svs_error_create(); - - svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); - svs_index_builder_h builder = svs_index_builder_create( - SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error - ); - svs_index_h index = svs_index_build_dynamic( - builder, data.data(), ids.data(), NUM_VECTORS, 0, error + builder, data.data(), ids.data(), NUM_VECTORS, BLOCK_SIZE, error ); CATCH_REQUIRE(index != nullptr); @@ -412,8 +331,46 @@ CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { CATCH_REQUIRE(deleted_count == 0); svs_index_free(index); - svs_index_builder_free(builder); - svs_algorithm_free(algorithm); - svs_error_free(error); } + + CATCH_SECTION("Dynamic Index Save and Load") { + svs_index_h index = svs_index_build_dynamic( + builder, data.data(), ids.data(), NUM_VECTORS, BLOCK_SIZE, error + ); + CATCH_REQUIRE(index != nullptr); + + // Create temporary directory for saving index + TempDir temp_dir; + auto temp_path = temp_dir.path(); + + // Save the index to disk + const char* directory = temp_path.c_str(); + bool success = svs_index_save(index, directory, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + + // Load the index back + svs_index_h loaded_index = + svs_index_load_dynamic(builder, directory, BLOCK_SIZE, error); + CATCH_REQUIRE(loaded_index != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + // Perform search on loaded index + std::vector queries; + generate_test_data(queries, 2, DIMENSION); + + svs_search_results_t results = + svs_index_search(loaded_index, queries.data(), 2, K, nullptr, error); + CATCH_REQUIRE(results != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(results->num_queries == 2); + + svs_search_results_free(results); + svs_index_free(loaded_index); + svs_index_free(index); + } + + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); } diff --git a/bindings/c/tests/c_api_index.cpp b/bindings/c/tests/c_api_index.cpp index eed471d8..b8b6245f 100644 --- a/bindings/c/tests/c_api_index.cpp +++ b/bindings/c/tests/c_api_index.cpp @@ -20,6 +20,9 @@ // catch2 #include "catch2/catch_test_macros.hpp" +// Test utilities +#include "c_api_test_utils.h" + // Standard library #include #include @@ -63,6 +66,7 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") const size_t NUM_QUERIES = 5; const size_t DIMENSION = 32; const size_t K = 10; + const size_t NUM_THREADS = 4; std::vector data; std::vector queries; @@ -84,6 +88,12 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") CATCH_REQUIRE(builder != nullptr); CATCH_REQUIRE(svs_error_ok(error)); + bool success = svs_index_builder_set_threadpool( + builder, SVS_THREADPOOL_KIND_NATIVE, NUM_THREADS, error + ); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + // Build index with default threadpool svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); CATCH_REQUIRE(index != nullptr); @@ -141,6 +151,11 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") ); CATCH_REQUIRE(builder != nullptr); + bool success = svs_index_builder_set_threadpool( + builder, SVS_THREADPOOL_KIND_NATIVE, NUM_THREADS, error + ); + CATCH_REQUIRE(success); + svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); CATCH_REQUIRE(index != nullptr); @@ -170,10 +185,15 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") ); CATCH_REQUIRE(builder != nullptr); + bool success = svs_index_builder_set_threadpool( + builder, SVS_THREADPOOL_KIND_NATIVE, NUM_THREADS, error + ); + CATCH_REQUIRE(success); + svs_storage_h storage = svs_storage_create_simple(SVS_DATA_TYPE_FLOAT16, error); CATCH_REQUIRE(storage != nullptr); - bool success = svs_index_builder_set_storage(builder, storage, error); + success = svs_index_builder_set_storage(builder, storage, error); CATCH_REQUIRE(success); svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); @@ -192,6 +212,70 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") svs_error_free(error); } + CATCH_SECTION("Index Basic Build and Search with Quantized Storages") { + svs_error_h error = svs_error_create(); + + auto run_build_and_search = [&](svs_storage_h storage) { + CATCH_REQUIRE(storage != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + CATCH_REQUIRE(algorithm != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + CATCH_REQUIRE(builder != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + bool success = svs_index_builder_set_threadpool( + builder, SVS_THREADPOOL_KIND_NATIVE, NUM_THREADS, error + ); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + + success = svs_index_builder_set_storage(builder, storage, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + + svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); + CATCH_REQUIRE(index != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + svs_search_results_t results = + svs_index_search(index, queries.data(), NUM_QUERIES, K, nullptr, error); + CATCH_REQUIRE(results != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(results->num_queries == NUM_QUERIES); + + for (size_t i = 0; i < NUM_QUERIES; ++i) { + CATCH_REQUIRE(results->results_per_query[i] == K); + } + + svs_search_results_free(results); + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_storage_free(storage); + }; + + // LeanVec: leanvec_dims = DIMENSION / 2, primary = int4, secondary = int8 + run_build_and_search(svs_storage_create_leanvec( + DIMENSION / 2, SVS_DATA_TYPE_INT4, SVS_DATA_TYPE_INT8, error + )); + + // LVQ: primary = int4, residual = int8 + run_build_and_search( + svs_storage_create_lvq(SVS_DATA_TYPE_INT4, SVS_DATA_TYPE_INT8, error) + ); + + // Scalar Quantization: int8 + run_build_and_search(svs_storage_create_sq(SVS_DATA_TYPE_INT8, error)); + + svs_error_free(error); + } + CATCH_SECTION("Index with Custom Threadpool") { svs_error_h error = svs_error_create(); @@ -233,12 +317,17 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error ); + bool success = svs_index_builder_set_threadpool( + builder, SVS_THREADPOOL_KIND_NATIVE, NUM_THREADS, error + ); + CATCH_REQUIRE(success); + svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); CATCH_REQUIRE(index != nullptr); // Get distance from first vector to first query float distance = -1.0f; - bool success = svs_index_get_distance(index, 0, queries.data(), &distance, error); + success = svs_index_get_distance(index, 0, queries.data(), &distance, error); CATCH_REQUIRE(success); CATCH_REQUIRE(svs_error_ok(error)); CATCH_REQUIRE(distance >= 0.0f); @@ -262,6 +351,11 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error ); + bool success = svs_index_builder_set_threadpool( + builder, SVS_THREADPOOL_KIND_NATIVE, NUM_THREADS, error + ); + CATCH_REQUIRE(success); + svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); CATCH_REQUIRE(index != nullptr); @@ -270,7 +364,7 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") size_t num_ids = 3; std::vector reconstructed(num_ids * DIMENSION); - bool success = svs_index_reconstruct( + success = svs_index_reconstruct( index, ids, num_ids, reconstructed.data(), DIMENSION, error ); CATCH_REQUIRE(success); @@ -300,6 +394,11 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error ); + bool success = svs_index_builder_set_threadpool( + builder, SVS_THREADPOOL_KIND_NATIVE, NUM_THREADS, error + ); + CATCH_REQUIRE(success); + svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); CATCH_REQUIRE(index != nullptr); @@ -333,6 +432,11 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error ); + bool success = svs_index_builder_set_threadpool( + builder, SVS_THREADPOOL_KIND_NATIVE, NUM_THREADS, error + ); + CATCH_REQUIRE(success); + svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); CATCH_REQUIRE(index != nullptr); @@ -351,6 +455,66 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") svs_algorithm_free(algorithm); svs_error_free(error); } + + CATCH_SECTION("Index Save and Load") { + svs_error_h error = svs_error_create(); + + // Create algorithm + svs_algorithm_h algorithm = svs_algorithm_create_vamana(16, 32, 50, error); + CATCH_REQUIRE(algorithm != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + // Create builder + svs_index_builder_h builder = svs_index_builder_create( + SVS_DISTANCE_METRIC_EUCLIDEAN, DIMENSION, algorithm, error + ); + CATCH_REQUIRE(builder != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + bool success = svs_index_builder_set_threadpool( + builder, SVS_THREADPOOL_KIND_NATIVE, NUM_THREADS, error + ); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + + // Build index with default threadpool + svs_index_h index = svs_index_build(builder, data.data(), NUM_VECTORS, error); + CATCH_REQUIRE(index != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + // Create temporary directory for saving index + TempDir temp_dir; + auto temp_path = temp_dir.path(); + + // Save the index to disk + const char* directory = temp_path.c_str(); + success = svs_index_save(index, directory, error); + CATCH_REQUIRE(success); + CATCH_REQUIRE(svs_error_ok(error)); + + // Load the index back + svs_index_h loaded_index = svs_index_load(builder, directory, error); + CATCH_REQUIRE(loaded_index != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + + // Perform search on loaded index + std::vector queries; + generate_test_data(queries, 2, DIMENSION); + + svs_search_results_t results = + svs_index_search(loaded_index, queries.data(), 2, K, nullptr, error); + CATCH_REQUIRE(results != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(results->num_queries == 2); + + // Cleanup + svs_search_results_free(results); + svs_index_free(loaded_index); + svs_index_free(index); + svs_index_builder_free(builder); + svs_algorithm_free(algorithm); + svs_error_free(error); + } } CATCH_TEST_CASE("C API Threadpool Management", "[c_api][index][threadpool]") { diff --git a/bindings/c/tests/c_api_test_utils.h b/bindings/c/tests/c_api_test_utils.h new file mode 100644 index 00000000..d89b287f --- /dev/null +++ b/bindings/c/tests/c_api_test_utils.h @@ -0,0 +1,68 @@ +// Copyright 2026 Intel Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include + +/// RAII wrapper that creates a unique temporary directory on construction +/// and removes it (recursively) on destruction. +class TempDir { + public: + TempDir() { + auto tmp = std::filesystem::temp_directory_path(); + // Create a unique directory using a template-style approach + std::string tmpl = (tmp / "svs_test_XXXXXX").string(); + if (::mkdtemp(tmpl.data()) == nullptr) { + throw std::runtime_error("Failed to create temporary directory"); + } + path_ = tmpl; + } + + ~TempDir() { + if (!path_.empty() && std::filesystem::exists(path_)) { + std::filesystem::remove_all(path_); + } + } + + // Non-copyable + TempDir(const TempDir&) = delete; + TempDir& operator=(const TempDir&) = delete; + + // Movable + TempDir(TempDir&& other) noexcept + : path_(std::move(other.path_)) { + other.path_.clear(); + } + TempDir& operator=(TempDir&& other) noexcept { + if (this != &other) { + if (!path_.empty() && std::filesystem::exists(path_)) { + std::filesystem::remove_all(path_); + } + path_ = std::move(other.path_); + other.path_.clear(); + } + return *this; + } + + const std::filesystem::path& path() const { return path_; } + std::string string() const { return path_.string(); } + + operator const std::filesystem::path&() const { return path_; } + + private: + std::filesystem::path path_; +}; From c6e70defe6d34f3962f92c7b64fcf4d72a11e451 Mon Sep 17 00:00:00 2001 From: Rafik Saliev Date: Wed, 10 Jun 2026 06:13:11 -0700 Subject: [PATCH 4/7] Address code review comments --- bindings/c/tests/README.md | 2 +- bindings/c/tests/c_api_dynamic_index.cpp | 24 --------- bindings/c/tests/c_api_index.cpp | 50 ++++------------- bindings/c/tests/c_api_index_builder.cpp | 41 +++++--------- bindings/c/tests/c_api_search_params.cpp | 11 ++-- bindings/c/tests/c_api_storage.cpp | 31 ++++++----- bindings/c/tests/c_api_test_utils.h | 69 ++++++++++++++++++++++++ 7 files changed, 115 insertions(+), 113 deletions(-) diff --git a/bindings/c/tests/README.md b/bindings/c/tests/README.md index d595c0d2..e65c6bf0 100644 --- a/bindings/c/tests/README.md +++ b/bindings/c/tests/README.md @@ -155,7 +155,7 @@ When adding new tests: - Catch2 v3.x (automatically fetched if not found) - SVS C API library -- C++17 or later compiler +- C++20 or later compiler ## Notes diff --git a/bindings/c/tests/c_api_dynamic_index.cpp b/bindings/c/tests/c_api_dynamic_index.cpp index abbc4443..d8a15ed1 100644 --- a/bindings/c/tests/c_api_dynamic_index.cpp +++ b/bindings/c/tests/c_api_dynamic_index.cpp @@ -24,32 +24,8 @@ #include "c_api_test_utils.h" // Standard library -#include #include -namespace { - -// Helper function to generate test data -void generate_test_data(std::vector& data, size_t num_vectors, size_t dimension) { - data.resize(num_vectors * dimension); - for (size_t i = 0; i < data.size(); ++i) { - data[i] = static_cast((i * 13) % 100) / 100.0f; - } -} - -// Sequential threadpool for testing -size_t sequential_tp_size(void* /*self*/) { return 1; } - -void sequential_tp_parallel_for( - void* /*self*/, void (*func)(void*, size_t), void* svs_param, size_t n -) { - for (size_t i = 0; i < n; ++i) { - func(svs_param, i); - } -} - -} // namespace - CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { const size_t NUM_VECTORS = 50; const size_t DIMENSION = 32; diff --git a/bindings/c/tests/c_api_index.cpp b/bindings/c/tests/c_api_index.cpp index b8b6245f..07dce2c4 100644 --- a/bindings/c/tests/c_api_index.cpp +++ b/bindings/c/tests/c_api_index.cpp @@ -25,42 +25,8 @@ // Standard library #include -#include #include -namespace { - -// Helper function to generate test data -void generate_test_data(std::vector& data, size_t num_vectors, size_t dimension) { - data.resize(num_vectors * dimension); - for (size_t i = 0; i < data.size(); ++i) { - data[i] = static_cast((i * 7) % 100) / 100.0f; - } -} - -// Helper to calculate Euclidean distance -float euclidean_distance(const float* a, const float* b, size_t dim) { - float sum = 0.0f; - for (size_t i = 0; i < dim; ++i) { - float diff = a[i] - b[i]; - sum += diff * diff; - } - return std::sqrt(sum); -} - -// Sequential threadpool for testing -size_t sequential_tp_size(void* /*self*/) { return 1; } - -void sequential_tp_parallel_for( - void* /*self*/, void (*func)(void*, size_t), void* svs_param, size_t n -) { - for (size_t i = 0; i < n; ++i) { - func(svs_param, i); - } -} - -} // namespace - CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") { const size_t NUM_VECTORS = 100; const size_t NUM_QUERIES = 5; @@ -261,17 +227,21 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") }; // LeanVec: leanvec_dims = DIMENSION / 2, primary = int4, secondary = int8 - run_build_and_search(svs_storage_create_leanvec( + svs_storage_h storage = svs_storage_create_leanvec( DIMENSION / 2, SVS_DATA_TYPE_INT4, SVS_DATA_TYPE_INT8, error - )); + ); + CATCH_REQUIRE(check_storage_support(storage, error) == true); + run_build_and_search(storage); // LVQ: primary = int4, residual = int8 - run_build_and_search( - svs_storage_create_lvq(SVS_DATA_TYPE_INT4, SVS_DATA_TYPE_INT8, error) - ); + storage = svs_storage_create_lvq(SVS_DATA_TYPE_INT4, SVS_DATA_TYPE_INT8, error); + CATCH_REQUIRE(check_storage_support(storage, error) == true); + run_build_and_search(storage); // Scalar Quantization: int8 - run_build_and_search(svs_storage_create_sq(SVS_DATA_TYPE_INT8, error)); + storage = svs_storage_create_sq(SVS_DATA_TYPE_INT8, error); + CATCH_REQUIRE(check_storage_support(storage, error) == true); + run_build_and_search(storage); svs_error_free(error); } diff --git a/bindings/c/tests/c_api_index_builder.cpp b/bindings/c/tests/c_api_index_builder.cpp index 5aaa8d13..3d77ae67 100644 --- a/bindings/c/tests/c_api_index_builder.cpp +++ b/bindings/c/tests/c_api_index_builder.cpp @@ -20,32 +20,12 @@ // catch2 #include "catch2/catch_test_macros.hpp" +// Test utilities +#include "c_api_test_utils.h" + // Standard library #include -namespace { - -// Helper function to generate random test data -void generate_test_data(std::vector& data, size_t num_vectors, size_t dimension) { - data.resize(num_vectors * dimension); - for (size_t i = 0; i < data.size(); ++i) { - data[i] = static_cast(i % 100) / 100.0f; - } -} - -// Sequential threadpool implementation for testing -size_t sequential_tp_size(void* /*self*/) { return 1; } - -void sequential_tp_parallel_for( - void* /*self*/, void (*func)(void*, size_t), void* svs_param, size_t n -) { - for (size_t i = 0; i < n; ++i) { - func(svs_param, i); - } -} - -} // namespace - CATCH_TEST_CASE("C API Index Builder", "[c_api][index_builder]") { CATCH_SECTION("Index Builder Creation") { svs_error_h error = svs_error_create(); @@ -212,13 +192,18 @@ CATCH_TEST_CASE("C API Index Builder", "[c_api][index_builder]") { svs_algorithm_h algorithm = svs_algorithm_create_vamana(64, 128, 100, error); CATCH_REQUIRE(algorithm != nullptr); - // Try to create with 0 dimension + // Try to create with 0 dimension: this must fail with INVALID_ARGUMENT svs_index_builder_h builder = svs_index_builder_create(SVS_DISTANCE_METRIC_EUCLIDEAN, 0, algorithm, error); - // Behavior depends on implementation - if (builder != nullptr) { - svs_index_builder_free(builder); - } + CATCH_REQUIRE(builder == nullptr); + CATCH_REQUIRE(svs_error_ok(error) == false); + + auto code = svs_error_get_code(error); + CATCH_REQUIRE(code == SVS_ERROR_INVALID_ARGUMENT); + + const char* msg = svs_error_get_message(error); + CATCH_REQUIRE(msg != nullptr); + CATCH_REQUIRE(msg[0] != '\0'); svs_algorithm_free(algorithm); svs_error_free(error); diff --git a/bindings/c/tests/c_api_search_params.cpp b/bindings/c/tests/c_api_search_params.cpp index 0ea7130c..a94111fd 100644 --- a/bindings/c/tests/c_api_search_params.cpp +++ b/bindings/c/tests/c_api_search_params.cpp @@ -78,10 +78,13 @@ CATCH_TEST_CASE("C API Search Parameters", "[c_api][search_params]") { // Try to create with size 0 svs_search_params_h params = svs_search_params_create_vamana(0, error); - // Behavior depends on implementation - either nullptr or valid handle - if (params != nullptr) { - svs_search_params_free(params); - } + CATCH_REQUIRE(params == nullptr); + CATCH_REQUIRE(svs_error_ok(error) == false); + CATCH_REQUIRE(svs_error_get_code(error) == SVS_ERROR_INVALID_ARGUMENT); + + const char* msg = svs_error_get_message(error); + CATCH_REQUIRE(msg != nullptr); + CATCH_REQUIRE(msg[0] != '\0'); svs_error_free(error); } diff --git a/bindings/c/tests/c_api_storage.cpp b/bindings/c/tests/c_api_storage.cpp index b11f4d72..22953e44 100644 --- a/bindings/c/tests/c_api_storage.cpp +++ b/bindings/c/tests/c_api_storage.cpp @@ -20,6 +20,9 @@ // catch2 #include "catch2/catch_test_macros.hpp" +// Test utilities +#include "c_api_test_utils.h" + CATCH_TEST_CASE("C API Storage", "[c_api][storage]") { CATCH_SECTION("Simple Storage Float32") { svs_error_h error = svs_error_create(); @@ -74,8 +77,7 @@ CATCH_TEST_CASE("C API Storage", "[c_api][storage]") { svs_storage_h storage = svs_storage_create_leanvec( leanvec_dims, SVS_DATA_TYPE_UINT8, SVS_DATA_TYPE_UINT8, error ); - CATCH_REQUIRE(storage != nullptr); - CATCH_REQUIRE(svs_error_ok(error) == true); + CATCH_REQUIRE(check_storage_support(storage, error) == true); svs_storage_free(storage); svs_error_free(error); @@ -88,8 +90,7 @@ CATCH_TEST_CASE("C API Storage", "[c_api][storage]") { svs_storage_h storage = svs_storage_create_leanvec( leanvec_dims, SVS_DATA_TYPE_UINT4, SVS_DATA_TYPE_UINT4, error ); - CATCH_REQUIRE(storage != nullptr); - CATCH_REQUIRE(svs_error_ok(error) == true); + CATCH_REQUIRE(check_storage_support(storage, error) == true); svs_storage_free(storage); svs_error_free(error); @@ -100,8 +101,7 @@ CATCH_TEST_CASE("C API Storage", "[c_api][storage]") { svs_storage_h storage = svs_storage_create_lvq(SVS_DATA_TYPE_UINT4, SVS_DATA_TYPE_VOID, error); - CATCH_REQUIRE(storage != nullptr); - CATCH_REQUIRE(svs_error_ok(error) == true); + CATCH_REQUIRE(check_storage_support(storage, error) == true); svs_storage_free(storage); svs_error_free(error); @@ -112,8 +112,7 @@ CATCH_TEST_CASE("C API Storage", "[c_api][storage]") { svs_storage_h storage = svs_storage_create_lvq(SVS_DATA_TYPE_UINT8, SVS_DATA_TYPE_VOID, error); - CATCH_REQUIRE(storage != nullptr); - CATCH_REQUIRE(svs_error_ok(error) == true); + CATCH_REQUIRE(check_storage_support(storage, error) == true); svs_storage_free(storage); svs_error_free(error); @@ -124,8 +123,7 @@ CATCH_TEST_CASE("C API Storage", "[c_api][storage]") { svs_storage_h storage = svs_storage_create_lvq(SVS_DATA_TYPE_UINT4, SVS_DATA_TYPE_UINT8, error); - CATCH_REQUIRE(storage != nullptr); - CATCH_REQUIRE(svs_error_ok(error) == true); + CATCH_REQUIRE(check_storage_support(storage, error) == true); svs_storage_free(storage); svs_error_free(error); @@ -135,8 +133,7 @@ CATCH_TEST_CASE("C API Storage", "[c_api][storage]") { svs_error_h error = svs_error_create(); svs_storage_h storage = svs_storage_create_sq(SVS_DATA_TYPE_UINT8, error); - CATCH_REQUIRE(storage != nullptr); - CATCH_REQUIRE(svs_error_ok(error) == true); + CATCH_REQUIRE(check_storage_support(storage, error) == true); svs_storage_free(storage); svs_error_free(error); @@ -146,8 +143,7 @@ CATCH_TEST_CASE("C API Storage", "[c_api][storage]") { svs_error_h error = svs_error_create(); svs_storage_h storage = svs_storage_create_sq(SVS_DATA_TYPE_INT8, error); - CATCH_REQUIRE(storage != nullptr); - CATCH_REQUIRE(svs_error_ok(error) == true); + CATCH_REQUIRE(check_storage_support(storage, error) == true); svs_storage_free(storage); svs_error_free(error); @@ -164,14 +160,17 @@ CATCH_TEST_CASE("C API Storage", "[c_api][storage]") { svs_error_h error = svs_error_create(); svs_storage_h storage1 = svs_storage_create_simple(SVS_DATA_TYPE_FLOAT32, error); + CATCH_REQUIRE(svs_error_ok(error) == true); + svs_storage_h storage2 = svs_storage_create_simple(SVS_DATA_TYPE_FLOAT16, error); + CATCH_REQUIRE(svs_error_ok(error) == true); + svs_storage_h storage3 = svs_storage_create_leanvec(64, SVS_DATA_TYPE_UINT8, SVS_DATA_TYPE_UINT8, error); CATCH_REQUIRE(storage1 != nullptr); CATCH_REQUIRE(storage2 != nullptr); - CATCH_REQUIRE(storage3 != nullptr); - CATCH_REQUIRE(svs_error_ok(error) == true); + CATCH_REQUIRE(check_storage_support(storage3, error) == true); svs_storage_free(storage1); svs_storage_free(storage2); diff --git a/bindings/c/tests/c_api_test_utils.h b/bindings/c/tests/c_api_test_utils.h index d89b287f..1a22289e 100644 --- a/bindings/c/tests/c_api_test_utils.h +++ b/bindings/c/tests/c_api_test_utils.h @@ -14,9 +14,14 @@ #pragma once +#include +#include +#include #include #include #include +#include +#include /// RAII wrapper that creates a unique temporary directory on construction /// and removes it (recursively) on destruction. @@ -66,3 +71,67 @@ class TempDir { private: std::filesystem::path path_; }; + +// Helper function to generate test data +inline void +generate_test_data(std::vector& data, size_t num_vectors, size_t dimension) { + data.resize(num_vectors * dimension); + for (size_t i = 0; i < data.size(); ++i) { + data[i] = static_cast((i * 7) % 100) / 100.0f; + } +} + +// Sequential threadpool for testing +inline size_t sequential_tp_size(void* /*self*/) { return 1; } + +inline void sequential_tp_parallel_for( + void* /*self*/, void (*func)(void*, size_t), void* svs_param, size_t n +) { + for (size_t i = 0; i < n; ++i) { + func(svs_param, i); + } +} + +// Helper to calculate Euclidean distance +inline float euclidean_distance(const float* a, const float* b, size_t dim) { + float sum = 0.0f; + for (size_t i = 0; i < dim; ++i) { + float diff = a[i] - b[i]; + sum += diff * diff; + } + return sum; +} + +// Helper to calculate Inner Product distance +inline float inner_product_distance(const float* a, const float* b, size_t dim) { + float sum = 0.0f; + for (size_t i = 0; i < dim; ++i) { + sum += a[i] * b[i]; + } + return sum; +} + +// Helper to calculate Cosine distance +inline float cosine_distance(const float* a, const float* b, size_t dim) { + float dot_product = 0.0f; + float norm_a = 0.0f; + float norm_b = 0.0f; + for (size_t i = 0; i < dim; ++i) { + dot_product += a[i] * b[i]; + norm_a += a[i] * a[i]; + norm_b += b[i] * b[i]; + } + if (norm_a == 0.0f || norm_b == 0.0f) { + return 1.0f; // Define cosine distance as 1 if either vector is zero + } + return dot_product / (std::sqrt(norm_a) * std::sqrt(norm_b)); +} + +inline bool check_storage_support(svs_storage_h storage, svs_error_h error) { + if (storage == nullptr) { + auto code = svs_error_get_code(error); + return code == SVS_ERROR_NOT_IMPLEMENTED || code == SVS_ERROR_UNSUPPORTED_HW; + } else { + return svs_error_ok(error) == true; + } +} From 540a9db453bcf13fd4294cdd85a097ca24733a18 Mon Sep 17 00:00:00 2001 From: Rafik Saliev Date: Wed, 10 Jun 2026 07:10:36 -0700 Subject: [PATCH 5/7] Code review: fix CMake configuration and dynamic index - Bump project version to 0.4.0 in CMakeLists.txt - Set default C++ standard to 20 if not defined - Change target_link_libraries to target_link_options for coverage - Improve delete_points method in DynamicIndex to handle non-existing IDs - Add tests for deleting existing and non-existing IDs in c_api_dynamic_index.cpp - Include C API header in test utilities for better integration --- bindings/c/CMakeLists.txt | 9 ++++++--- bindings/c/src/index.hpp | 20 +++++++++++++++----- bindings/c/src/svs_c.cpp | 15 ++------------- bindings/c/tests/c_api_dynamic_index.cpp | 10 +++++++++- bindings/c/tests/c_api_test_utils.h | 14 ++++++++++---- 5 files changed, 42 insertions(+), 26 deletions(-) diff --git a/bindings/c/CMakeLists.txt b/bindings/c/CMakeLists.txt index ab4f6b7c..dd9847d2 100644 --- a/bindings/c/CMakeLists.txt +++ b/bindings/c/CMakeLists.txt @@ -13,7 +13,7 @@ # limitations under the License. cmake_minimum_required(VERSION 3.21) -project(svs_c_api VERSION 0.1.0 LANGUAGES CXX C) +project(svs_c_api VERSION 0.4.0 LANGUAGES CXX C) set(TARGET_NAME svs_c_api) set(SVS_C_API_HEADERS @@ -60,8 +60,11 @@ if(UNIX AND NOT APPLE) endif() target_compile_features(${TARGET_NAME} INTERFACE cxx_std_20) +if (NOT DEFINED SVS_CXX_STANDARD OR SVS_CXX_STANDARD STREQUAL "") + set(SVS_CXX_STANDARD 20) +endif() set_target_properties(${TARGET_NAME} PROPERTIES PUBLIC_HEADER "${SVS_C_API_HEADERS}") -set_target_properties(${TARGET_NAME} PROPERTIES CXX_STANDARD 20) +set_target_properties(${TARGET_NAME} PROPERTIES CXX_STANDARD ${SVS_CXX_STANDARD}) set_target_properties(${TARGET_NAME} PROPERTIES CXX_STANDARD_REQUIRED ON) set_target_properties(${TARGET_NAME} PROPERTIES CXX_EXTENSIONS OFF) set_target_properties(${TARGET_NAME} PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR} ) @@ -200,7 +203,7 @@ endif() if(SVS_BUILD_C_API_TESTS) if(CMAKE_BUILD_TYPE STREQUAL "Debug") target_compile_options(${TARGET_NAME} PRIVATE --coverage) - target_link_libraries(${TARGET_NAME} PRIVATE --coverage) + target_link_options(${TARGET_NAME} PRIVATE --coverage) # add coverage target add_custom_target(clean_coverage COMMAND ${CMAKE_COMMAND} -E echo "Cleaning coverage data..." diff --git a/bindings/c/src/index.hpp b/bindings/c/src/index.hpp index 60617ae3..c38d0c6d 100644 --- a/bindings/c/src/index.hpp +++ b/bindings/c/src/index.hpp @@ -23,12 +23,14 @@ #include #include #include +#include #include #include #include #include #include +#include namespace svs::c_runtime { struct Index { @@ -155,11 +157,19 @@ struct DynamicIndexVamana : public DynamicIndex { } size_t delete_points(std::span ids) override { - auto old_size = index.size(); - index.delete_points(ids); - // TODO: This is a bit of a hack - we should ideally return the number of points - // actually deleted, but for now we can just return index size change. - return old_size - index.size(); + std::vector ids_to_delete; + ids_to_delete.reserve(ids.size()); + + for (auto id : ids) { + if (index.has_id(id)) { + ids_to_delete.push_back(id); + } + } + + if (!ids_to_delete.empty()) { + index.delete_points(svs::lib::as_const_span(ids_to_delete)); + } + return ids_to_delete.size(); } bool has_id(size_t id) const override { return index.has_id(id); } diff --git a/bindings/c/src/svs_c.cpp b/bindings/c/src/svs_c.cpp index d67523dc..f2a3a312 100644 --- a/bindings/c/src/svs_c.cpp +++ b/bindings/c/src/svs_c.cpp @@ -662,7 +662,7 @@ extern "C" size_t svs_index_dynamic_delete_points( ) { using namespace svs::c_runtime; std::shared_ptr dynamic_index_ptr; - auto result = wrap_exceptions( + return wrap_exceptions( [&]() { EXPECT_ARG_NOT_NULL(index); EXPECT_ARG_NOT_NULL(ids); @@ -671,22 +671,11 @@ extern "C" size_t svs_index_dynamic_delete_points( INVALID_ARGUMENT_IF( dynamic_index_ptr == nullptr, "Index does not support dynamic updates" ); - return 0; // return 0 for success, actual deletion happens in the next - // wrap_exceptions call + return dynamic_index_ptr->delete_points(std::span(ids, num_ids)); }, out_err, static_cast(-1) ); - if (result != 0) { - return result; - } - // Call delete_points in a separate wrap_exceptions to return 0 if no entries are - // deleted. - return wrap_exceptions( - [&]() { return dynamic_index_ptr->delete_points(std::span(ids, num_ids)); }, - out_err, - 0 - ); } extern "C" bool svs_index_dynamic_has_id( diff --git a/bindings/c/tests/c_api_dynamic_index.cpp b/bindings/c/tests/c_api_dynamic_index.cpp index d8a15ed1..050726c3 100644 --- a/bindings/c/tests/c_api_dynamic_index.cpp +++ b/bindings/c/tests/c_api_dynamic_index.cpp @@ -303,9 +303,17 @@ CATCH_TEST_CASE("C API Dynamic Index", "[c_api][index][dynamic]") { size_t non_existing_id = NUM_VECTORS + 1000; size_t deleted_count = svs_index_dynamic_delete_points(index, &non_existing_id, 1, error); - // Should return 0 for non-existing ID + // Should return 0 for non-existing ID and no error + CATCH_REQUIRE(svs_error_ok(error)); CATCH_REQUIRE(deleted_count == 0); + // Try to delete mix of existing and non-existing IDs + size_t ids_to_delete[] = {0, non_existing_id}; + deleted_count = svs_index_dynamic_delete_points(index, ids_to_delete, 2, error); + // Should return 1 for the existing ID and no error + CATCH_REQUIRE(svs_error_ok(error)); + CATCH_REQUIRE(deleted_count == 1); + svs_index_free(index); } diff --git a/bindings/c/tests/c_api_test_utils.h b/bindings/c/tests/c_api_test_utils.h index 1a22289e..c1f488f4 100644 --- a/bindings/c/tests/c_api_test_utils.h +++ b/bindings/c/tests/c_api_test_utils.h @@ -14,6 +14,10 @@ #pragma once +// C API +#include "svs/c_api/svs_c.h" + +// Standard library #include #include #include @@ -38,8 +42,9 @@ class TempDir { } ~TempDir() { - if (!path_.empty() && std::filesystem::exists(path_)) { - std::filesystem::remove_all(path_); + if (!path_.empty()) { + std::error_code ec; + std::filesystem::remove_all(path_, ec); } } @@ -54,8 +59,9 @@ class TempDir { } TempDir& operator=(TempDir&& other) noexcept { if (this != &other) { - if (!path_.empty() && std::filesystem::exists(path_)) { - std::filesystem::remove_all(path_); + if (!path_.empty()) { + std::error_code ec; + std::filesystem::remove_all(path_, ec); } path_ = std::move(other.path_); other.path_.clear(); From 93013a98d407ba1ac0833da05fd8340ddfe9e555 Mon Sep 17 00:00:00 2001 From: Rafik Saliev Date: Wed, 10 Jun 2026 07:28:46 -0700 Subject: [PATCH 6/7] Update CMake configuration for C API tests --- bindings/c/tests/CMakeLists.txt | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/bindings/c/tests/CMakeLists.txt b/bindings/c/tests/CMakeLists.txt index 537e3529..18645069 100644 --- a/bindings/c/tests/CMakeLists.txt +++ b/bindings/c/tests/CMakeLists.txt @@ -29,7 +29,9 @@ if(NOT Catch2_FOUND) set(CATCH_CONFIG_PREFIX_ALL ON CACHE BOOL "" FORCE) set(PRESET_CMAKE_CXX_STANDARD ${CMAKE_CXX_STANDARD}) - set(CMAKE_CXX_STANDARD ${SVS_CXX_STANDARD}) + if(DEFINED SVS_CXX_STANDARD) + set(CMAKE_CXX_STANDARD ${SVS_CXX_STANDARD}) + endif() FetchContent_Declare( Catch2 GIT_REPOSITORY https://github.com/catchorg/Catch2.git @@ -76,14 +78,6 @@ target_include_directories(${TARGET_NAME} PRIVATE include(CTest) enable_testing() -# Add the test to CTest -add_test(NAME ${TARGET_NAME} COMMAND ${TARGET_NAME}) - -# Set test properties -set_tests_properties(${TARGET_NAME} PROPERTIES - LABELS "c_api" -) - # Add Catch2 CMake module path if(NOT Catch2_FOUND) # Catch2 was fetched, use its source directory @@ -94,7 +88,7 @@ else() endif() include(Catch) -catch_discover_tests(${TARGET_NAME}) +catch_discover_tests(${TARGET_NAME} PROPERTIES LABELS "c_api") # Add a custom target to run tests add_custom_target(run_c_api_test From 70c02b3e837ece38297af178434d3493ee31bcc4 Mon Sep 17 00:00:00 2001 From: Rafik Saliev Date: Wed, 10 Jun 2026 07:37:21 -0700 Subject: [PATCH 7/7] revert svs_c.cpp to the base branch state --- bindings/c/src/svs_c.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bindings/c/src/svs_c.cpp b/bindings/c/src/svs_c.cpp index f2a3a312..85f2fa3e 100644 --- a/bindings/c/src/svs_c.cpp +++ b/bindings/c/src/svs_c.cpp @@ -661,13 +661,12 @@ extern "C" size_t svs_index_dynamic_delete_points( svs_index_h index, const size_t* ids, size_t num_ids, svs_error_h out_err ) { using namespace svs::c_runtime; - std::shared_ptr dynamic_index_ptr; return wrap_exceptions( [&]() { EXPECT_ARG_NOT_NULL(index); EXPECT_ARG_NOT_NULL(ids); EXPECT_ARG_GT_THAN(num_ids, 0); - dynamic_index_ptr = std::dynamic_pointer_cast(index->impl); + auto dynamic_index_ptr = std::dynamic_pointer_cast(index->impl); INVALID_ARGUMENT_IF( dynamic_index_ptr == nullptr, "Index does not support dynamic updates" );