diff --git a/CMakeLists.txt b/CMakeLists.txt index 100b5ba..1223e9b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,4 +16,12 @@ add_subdirectory(sample) add_library(rabitq_headers INTERFACE) -target_include_directories(rabitq_headers INTERFACE ${PROJECT_SOURCE_DIR}/include) \ No newline at end of file +target_include_directories(rabitq_headers INTERFACE ${PROJECT_SOURCE_DIR}/include) + +# Testing +option(RABITQ_BUILD_TESTS "Build tests" OFF) + +if(RABITQ_BUILD_TESTS) + enable_testing() + add_subdirectory(tests) +endif() \ No newline at end of file diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..a87b94a --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +/googletest \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..1de1651 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,51 @@ +cmake_minimum_required(VERSION 3.10) + +# Fetch Google Test +include(FetchContent) +FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG v1.14.0 +) +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(googletest) + +enable_testing() + +# Include directories +include_directories(${PROJECT_SOURCE_DIR}/include) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/common) + +# Compiler flags +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra") + +# Automatically register all tests under unit and folders +file(GLOB_RECURSE UNIT_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/unit/*_test.cpp) +file(GLOB_RECURSE INTEG_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/integration/*_test.cpp) +file(GLOB_RECURSE COMMON_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/common/*.cpp) + +# add executables +add_executable(rabitq_tests + ${COMMON_SRCS} + ${UNIT_TESTS} + ${INTEG_TESTS} +) + +# Link google test and rabitqlib headers +target_link_libraries(rabitq_tests + gtest + gtest_main + rabitq_headers + pthread +) + +# Discover tests for CTest +include(GoogleTest) +gtest_discover_tests(rabitq_tests) + +# Message to verify files are found +message(STATUS "Discovered test files:") +foreach(TEST_SRC ${ALL_TEST_SRCS}) + file(RELATIVE_PATH REL_PATH ${CMAKE_CURRENT_SOURCE_DIR} ${TEST_SRC}) + message(STATUS " - ${REL_PATH}") +endforeach() \ No newline at end of file diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..bf33e3e --- /dev/null +++ b/tests/README.md @@ -0,0 +1,74 @@ +# RaBitQ Testing Framework + +This directory contains the comprehensive testing framework for the RaBitQ library + +## Prerequisites + +- CMake 3.10 or higher +- C++17 compatible compiler (GCC, Clang, or MSVC) +- Google Test (automatically downloaded via CMake FetchContent) + +### Installing CMake + +For macos +```bash +brew install cmake +``` + +for (Ubuntu/Debian) + +```bash +sudo apt-get update +sudo apt-get install cmake +``` + +## Building and Running Tests + +### Quick Start + +From the project root directory: + +```bash +# Create build directory +mkdir build bin +cd build + +# Configure with tests enabled (tests are OFF by default) +cmake .. -DRABITQ_BUILD_TESTS=ON + +# Build the tests +make -j$(nproc) + +# Run all tests +./tests/rabitq_tests + +# Or use CTest for detailed output +ctest --output-on-failure +``` + +### Building without Tests + +By default, tests are **not built**. If you want to build only the library: + +```bash +cmake .. +``` + + +## Test Structure + +``` +tests/ +├── CMakeLists.txt # Automatic test discovery & suite configuration +├── main.cpp # Test runner entry point +├── common/ # Test utilities and helpers +│ ├── test_data.hpp # Test data generation utilities +│ ├── test_data.cpp +│ └── test_helpers.hpp # Custom assertions and helpers +├── unit/ # Unit tests (auto-discovered) +├── integration/ # Integration tests (auto-discovered) +└── benchmark/ # Performance benchmarks (to be added) +``` + +## Test Coverage + diff --git a/tests/common/test_data.cpp b/tests/common/test_data.cpp new file mode 100644 index 0000000..a38ce51 --- /dev/null +++ b/tests/common/test_data.cpp @@ -0,0 +1,103 @@ +#include "test_data.hpp" +#include +#include + +namespace rabitq_test { + +std::vector TestDataGenerator::GenerateRandomVector( + size_t dim, + float min, + float max, + unsigned int seed +) { + std::mt19937 rng(seed); + std::uniform_real_distribution dist(min, max); + + std::vector vec(dim); + for (size_t i = 0; i < dim; ++i) { + vec[i] = dist(rng); + } + return vec; +} + +std::vector TestDataGenerator::GenerateNormalizedVector( + size_t dim, + unsigned int seed +) { + auto vec = GenerateRandomVector(dim, -1.0f, 1.0f, seed); + + // Calculate L2 norm + float norm = 0.0f; + for (float val : vec) { + norm += val * val; + } + norm = std::sqrt(norm); + + // Normalize + if (norm > 1e-10f) { + for (float& val : vec) { + val /= norm; + } + } + + return vec; +} + +std::vector> TestDataGenerator::GenerateRandomVectors( + size_t num_vectors, + size_t dim, + float min, + float max, + unsigned int seed +) { + std::vector> vectors; + vectors.reserve(num_vectors); + + for (size_t i = 0; i < num_vectors; ++i) { + vectors.push_back(GenerateRandomVector(dim, min, max, seed + i)); + } + + return vectors; +} + +std::vector TestDataGenerator::GenerateGaussianVector( + size_t dim, + float mean, + float stddev, + unsigned int seed +) { + std::mt19937 rng(seed); + std::normal_distribution dist(mean, stddev); + + std::vector vec(dim); + for (size_t i = 0; i < dim; ++i) { + vec[i] = dist(rng); + } + return vec; +} + +std::vector TestDataGenerator::GenerateSimpleVector(size_t dim) { + std::vector vec(dim); + for (size_t i = 0; i < dim; ++i) { + vec[i] = static_cast(i % 10) / 10.0f; + } + return vec; +} + +std::vector TestDataGenerator::GenerateZeroVector(size_t dim) { + return std::vector(dim, 0.0f); +} + +std::vector TestDataGenerator::GenerateOnesVector(size_t dim) { + return std::vector(dim, 1.0f); +} + +std::vector TestDataGenerator::GenerateIncrementalVector(size_t dim) { + std::vector vec(dim); + for (size_t i = 0; i < dim; ++i) { + vec[i] = static_cast(i); + } + return vec; +} + +} // namespace rabitq_test diff --git a/tests/common/test_data.hpp b/tests/common/test_data.hpp new file mode 100644 index 0000000..65f6ef5 --- /dev/null +++ b/tests/common/test_data.hpp @@ -0,0 +1,58 @@ +#ifndef RABITQ_TEST_DATA_HPP +#define RABITQ_TEST_DATA_HPP + +#include +#include +#include + +namespace rabitq_test { + +class TestDataGenerator { +public: + // Generate random float vector with values in [min, max] + static std::vector GenerateRandomVector( + size_t dim, + float min = -1.0f, + float max = 1.0f, + unsigned int seed = 42 + ); + + // Generate random normalized vector (unit length) + static std::vector GenerateNormalizedVector( + size_t dim, + unsigned int seed = 42 + ); + + // Generate multiple random vectors + static std::vector> GenerateRandomVectors( + size_t num_vectors, + size_t dim, + float min = -1.0f, + float max = 1.0f, + unsigned int seed = 42 + ); + + // Generate Gaussian distributed vector + static std::vector GenerateGaussianVector( + size_t dim, + float mean = 0.0f, + float stddev = 1.0f, + unsigned int seed = 42 + ); + + // Generate a simple test vector with known values + static std::vector GenerateSimpleVector(size_t dim); + + // Generate zero vector + static std::vector GenerateZeroVector(size_t dim); + + // Generate vector with all ones + static std::vector GenerateOnesVector(size_t dim); + + // Generate vector with incremental values [0, 1, 2, 3, ...] + static std::vector GenerateIncrementalVector(size_t dim); +}; + +} // namespace rabitq_test + +#endif // RABITQ_TEST_DATA_HPP diff --git a/tests/common/test_helpers.hpp b/tests/common/test_helpers.hpp new file mode 100644 index 0000000..35d5052 --- /dev/null +++ b/tests/common/test_helpers.hpp @@ -0,0 +1,84 @@ +#ifndef RABITQ_TEST_HELPERS_HPP +#define RABITQ_TEST_HELPERS_HPP + +#include +#include +#include +#include + +namespace rabitq_test { + +// Floating point comparison with tolerance +inline bool FloatNearlyEqual(float a, float b, float epsilon = 1e-5f) { + return std::abs(a - b) < epsilon; +} + +inline bool DoubleNearlyEqual(double a, double b, double epsilon = 1e-10) { + return std::abs(a - b) < epsilon; +} + +// Vector comparison +inline bool VectorsNearlyEqual(const float* a, const float* b, size_t size, float epsilon = 1e-5f) { + for (size_t i = 0; i < size; ++i) { + if (!FloatNearlyEqual(a[i], b[i], epsilon)) { + return false; + } + } + return true; +} + +// Calculate relative error +inline float RelativeError(float actual, float expected) { + if (std::abs(expected) < 1e-10f) { + return std::abs(actual - expected); + } + return std::abs((actual - expected) / expected); +} + +// Calculate mean squared error +inline float MeanSquaredError(const float* a, const float* b, size_t size) { + float mse = 0.0f; + for (size_t i = 0; i < size; ++i) { + float diff = a[i] - b[i]; + mse += diff * diff; + } + return mse / size; +} + +// Calculate dot product +inline float DotProduct(const float* a, const float* b, size_t size) { + float result = 0.0f; + for (size_t i = 0; i < size; ++i) { + result += a[i] * b[i]; + } + return result; +} + +// Calculate L2 distance +inline float L2Distance(const float* a, const float* b, size_t size) { + float sum = 0.0f; + for (size_t i = 0; i < size; ++i) { + float diff = a[i] - b[i]; + sum += diff * diff; + } + return std::sqrt(sum); +} + +// Custom assertion macros +#define ASSERT_FLOAT_NEARLY_EQUAL(a, b, epsilon) \ + ASSERT_TRUE(rabitq_test::FloatNearlyEqual(a, b, epsilon)) \ + << "Expected: " << a << " to be nearly equal to " << b \ + << " (epsilon: " << epsilon << "), but difference was " << std::abs(a - b) + +#define EXPECT_FLOAT_NEARLY_EQUAL(a, b, epsilon) \ + EXPECT_TRUE(rabitq_test::FloatNearlyEqual(a, b, epsilon)) \ + << "Expected: " << a << " to be nearly equal to " << b \ + << " (epsilon: " << epsilon << "), but difference was " << std::abs(a - b) + +#define ASSERT_VECTORS_NEARLY_EQUAL(a, b, size, epsilon) \ + ASSERT_TRUE(rabitq_test::VectorsNearlyEqual(a, b, size, epsilon)) \ + << "Vectors are not nearly equal (epsilon: " << epsilon << ")" + +} // namespace rabitq_test + +#endif // RABITQ_TEST_HELPERS_HPP diff --git a/tests/integration/bit_pack_unpack_test.cpp b/tests/integration/bit_pack_unpack_test.cpp new file mode 100644 index 0000000..ead1766 --- /dev/null +++ b/tests/integration/bit_pack_unpack_test.cpp @@ -0,0 +1,137 @@ +#include +#include +#include +#include +#include +#include + +using namespace rabitqlib; + +class BitPackUnpackTest : public ::testing::Test { +protected: + // 1. Shared Constants & Data Structures + const size_t dim = 768; + std::vector query; + std::vector code; + std::vector compact_code; + + // 2. Common Initialization (Runs before EVERY test) + void SetUp() override { + srand(42); + + // Initialize Query (Same for all tests) + query.resize(dim); + for(size_t i = 0; i < dim; ++i) { + query[i] = static_cast((rand() * 100.0) / RAND_MAX); + } + + // Pre-allocate code vector + code.resize(dim); + } + + // 3. Helper to handle bit-specific setup + void PrepareData(size_t bits) { + // Resize compact code based on bits + compact_code.resize(dim * bits / 8 + 1); + + // Generate random codes based on bit depth + for (size_t i = 0; i < dim; ++i) { + code[i] = rand() % (1 << bits); + } + + // Pack the code + rabitqlib::quant::rabitq_impl::ex_bits::packing_rabitqplus_code( + code.data(), compact_code.data(), dim, bits + ); + } + + // 4. Helper for Ground Truth Calculation + float CalculateExpected() const { + float expected_result = 0.0f; + for (size_t i = 0; i < dim; ++i) { + expected_result += query[i] * code[i]; + } + return expected_result; + } +}; + +// --- Test Cases --- + +TEST_F(BitPackUnpackTest, ExCode1Bit) { + PrepareData(1); // Set up for 1-bit + + // Run the AVX function + float result = rabitqlib::excode_ipimpl::ip16_fxu1_avx( + query.data(), compact_code.data(), dim + ); + + ASSERT_NEAR(CalculateExpected(), result, 0.1); +} + +TEST_F(BitPackUnpackTest, ExCode2Bit) { + PrepareData(2); // Set up for 2-bit + + // Run the AVX function + float result = rabitqlib::excode_ipimpl::ip64_fxu2_avx( + query.data(), compact_code.data(), dim + ); + + ASSERT_NEAR(CalculateExpected(), result, 0.1); +} + +TEST_F(BitPackUnpackTest, ExCode3Bit) { + PrepareData(3); // Set up for 3-bit + + // Run the AVX function + float result = rabitqlib::excode_ipimpl::ip64_fxu3_avx( + query.data(), compact_code.data(), dim + ); + + ASSERT_NEAR(CalculateExpected(), result, 0.1); +} + +TEST_F(BitPackUnpackTest, ExCode4Bit) { + PrepareData(4); // Set up for 4-bit + + // Run the AVX function + float result = rabitqlib::excode_ipimpl::ip16_fxu4_avx( + query.data(), compact_code.data(), dim + ); + + ASSERT_NEAR(CalculateExpected(), result, 0.1); +} + +TEST_F(BitPackUnpackTest, ExCode5Bit) { + PrepareData(5); // Set up for 5-bit + + // Run the AVX function + float result = rabitqlib::excode_ipimpl::ip64_fxu5_avx( + query.data(), compact_code.data(), dim + ); + + ASSERT_NEAR(CalculateExpected(), result, 0.1); +} + +TEST_F(BitPackUnpackTest, ExCode6Bit) { + PrepareData(6); // Set up for 6-bit + + // Run the AVX function + float result = rabitqlib::excode_ipimpl::ip64_fxu6_avx( + query.data(), compact_code.data(), dim + ); + + ASSERT_NEAR(CalculateExpected(), result, 0.1); +} + +TEST_F(BitPackUnpackTest, ExCode7Bit) { + PrepareData(7); // Set up for 7-bit + + // Run the AVX function + float result = rabitqlib::excode_ipimpl::ip64_fxu7_avx( + query.data(), compact_code.data(), dim + ); + + ASSERT_NEAR(CalculateExpected(), result, 0.1); +} + + diff --git a/tests/unit/rabitqlib/utils/rotator_test.cpp b/tests/unit/rabitqlib/utils/rotator_test.cpp new file mode 100644 index 0000000..160e49c --- /dev/null +++ b/tests/unit/rabitqlib/utils/rotator_test.cpp @@ -0,0 +1,77 @@ +#include +#include "rabitqlib/utils/rotator.hpp" +#include "test_helpers.hpp" +#include "test_data.hpp" +#include +#include +#include +#include + +using namespace rabitqlib; +using namespace rabitq_test; + +class RotatorTest : public ::testing::Test { +protected: + void SetUp() override { + dim = 128; + test_data = TestDataGenerator::GenerateRandomVector(dim, -1.0f, 1.0f, 42); + } + + void TearDown() override { + // Clean up any temporary files + std::remove("test_rotator.bin"); + } + + size_t dim; + std::vector test_data; +}; + +// Test that FhtKacRotator is chosen by default +TEST_F(RotatorTest, DefaultRotatorType) { + Rotator* rotator = choose_rotator(dim); + ASSERT_NE(rotator, nullptr); + + // FhtKacRotator pads to multiple of 64 + size_t padded_dim = rotator->size(); + EXPECT_EQ(padded_dim % 64, 0); + EXPECT_GE(padded_dim, dim); + + delete rotator; +} + +uint8_t bitreverse8(uint8_t x) { + x = (((x & 0x55) << 1) | ((x & 0xAA) >> 1)); + x = (((x & 0x33) << 2) | ((x & 0xCC) >> 2)); + x = (((x & 0x0F) << 4) | ((x & 0xF0) >> 4)); + return x; +} +TEST(FlipSignTest, FlipWorks) { + const size_t dim = 128; + float data[dim]; + uint8_t flip[dim / 8]; // 1 bit per float + + // Initialize data and flip pattern + for (size_t i = 0; i < dim; ++i) { + data[i] = static_cast(i + 1); // Example data + } + for (size_t i = 0; i < dim / 8; ++i) { + flip[i] = static_cast(i % 256); // Example flip pattern + } + + // Perform sign flipping + rabitqlib::rotator_impl::flip_sign(flip, data, dim); + + // Output the results + uint8_t signs = 0; + for (size_t i = 0; i < dim; ++i) { + ASSERT_EQ(abs(data[i]), static_cast(i + 1)); + int sign = (data[i] < 0) ? 1 : 0; + signs = (signs << 1) | sign; + if(i%8 == 7) { + uint8_t expected = flip[i / 8]; + signs = bitreverse8(signs); + ASSERT_EQ(static_cast(signs & 0xFF), expected); + signs = 0; + } + } +} diff --git a/tests/unit/rabitqlib/utils/space_test.cpp b/tests/unit/rabitqlib/utils/space_test.cpp new file mode 100644 index 0000000..ec1e616 --- /dev/null +++ b/tests/unit/rabitqlib/utils/space_test.cpp @@ -0,0 +1,81 @@ +#include +#include "rabitqlib/utils/space.hpp" +#include "rabitqlib/defines.hpp" +#include "test_helpers.hpp" +#include "test_data.hpp" +#include +#include + +using namespace rabitqlib; +using namespace rabitq_test; + +TEST(Select_IP_Func, returns_correct_function_pointer) { + auto ip_func = select_excode_ipfunc(0); + ASSERT_NE(ip_func, nullptr); + ASSERT_EQ(ip_func, excode_ipimpl::ip16_fxu1_avx); + + ip_func = select_excode_ipfunc(1); + ASSERT_NE(ip_func, nullptr); + ASSERT_EQ(ip_func, excode_ipimpl::ip16_fxu1_avx); + + ip_func = select_excode_ipfunc(2); + ASSERT_NE(ip_func, nullptr); + ASSERT_EQ(ip_func, excode_ipimpl::ip64_fxu2_avx); + + ip_func = select_excode_ipfunc(3); + ASSERT_NE(ip_func, nullptr); + ASSERT_EQ(ip_func, excode_ipimpl::ip64_fxu3_avx); + + ip_func = select_excode_ipfunc(4); + ASSERT_NE(ip_func, nullptr); + ASSERT_EQ(ip_func, excode_ipimpl::ip16_fxu4_avx); + + ip_func = select_excode_ipfunc(5); + ASSERT_NE(ip_func, nullptr); + ASSERT_EQ(ip_func, excode_ipimpl::ip64_fxu5_avx); + + ip_func = select_excode_ipfunc(6); + ASSERT_NE(ip_func, nullptr); + ASSERT_EQ(ip_func, excode_ipimpl::ip64_fxu6_avx); + + ip_func = select_excode_ipfunc(7); + ASSERT_NE(ip_func, nullptr); + ASSERT_EQ(ip_func, excode_ipimpl::ip64_fxu7_avx); + + ip_func = select_excode_ipfunc(8); + ASSERT_NE(ip_func, nullptr); + ASSERT_EQ(ip_func, (excode_ipimpl::ip_fxi)); +} + +TEST(ip16_fxu1_avx, ip_works) { + srand(42); + size_t dim = 64; + float query[dim]; + uint8_t codes[dim/8]; + + for (size_t i = 0; i < dim; ++i) { + query[i] = static_cast(rand()) / RAND_MAX * 1000.0f; + } + + for (size_t i = 0; i < dim / 8; ++i) { + codes[i] = static_cast(rand() % 256); + } + + ASSERT_NEAR(rabitqlib::excode_ipimpl::ip16_fxu1_avx(query, codes, dim), 15055.81f, 0.1f); +} + +TEST(ip64_fxu2_avx, ip_works) { + srand(42); + size_t dim = 64*4; + float query[dim]; + uint8_t codes[dim/4]; + + for (size_t i = 0; i < dim; ++i) { + query[i] = static_cast(rand()) / RAND_MAX * 1000.0f; + } + + for (size_t i = 0; i < dim / 4; ++i) { + codes[i] = static_cast(rand() % 256); + } + ASSERT_NEAR(rabitqlib::excode_ipimpl::ip64_fxu2_avx(query, codes, dim), 217584.15f, 0.1f); +} \ No newline at end of file