Skip to content

Commit f567928

Browse files
committed
refactor(test): consolidate shared test utilities into common/test_helpers.h
Extract all shared test utilities (PRNG, dataset generation, groundtruth computation, checksum functions, integration and regression test runners) from test_regression.h and test_builder_integration.h into a single common/test_helpers.h header. Delete the two old headers and update all test source files and CMakeLists.txt to use the new shared include path. - Create cpp/test/src/common/test_helpers.h with all consolidated utilities - Delete cpp/test/src/regression/test_regression.h - Delete cpp/test/src/integration/test_builder_integration.h - Update test_builder_integration.cpp, test_distances_integration.cpp, and test_builder_regression.cpp to include common/test_helpers.h - Update CMakeLists.txt macros to use src/ as include directory - Build and verify: all 3 CTest targets pass, all 15 regression tests pass
1 parent 8b26fd7 commit f567928

11 files changed

Lines changed: 530 additions & 1646 deletions

cpp/test/CMakeLists.txt

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ macro(add_deglib_test name source)
1313
add_executable(${name} ${TEST_MAIN} ${source})
1414
target_include_directories(${name} PRIVATE ${DEGLIB_INCLUDE})
1515
target_include_directories(${name} PRIVATE ${GTEST_INCLUDE})
16-
target_include_directories(${name} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/regression)
16+
target_include_directories(${name} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
1717
target_sources(${name} PRIVATE ${GTEST_SOURCE})
1818
target_link_libraries(${name} PRIVATE compile-options)
1919
set_target_properties(${name} PROPERTIES INTERMEDIATE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/obj/${name}")
@@ -25,7 +25,7 @@ macro(add_deglib_integration_test name source)
2525
add_executable(${name} ${TEST_MAIN} ${source})
2626
target_include_directories(${name} PRIVATE ${DEGLIB_INCLUDE})
2727
target_include_directories(${name} PRIVATE ${GTEST_INCLUDE})
28-
target_include_directories(${name} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/integration)
28+
target_include_directories(${name} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
2929
target_sources(${name} PRIVATE ${GTEST_SOURCE})
3030
target_link_libraries(${name} PRIVATE compile-options)
3131
set_target_properties(${name} PROPERTIES INTERMEDIATE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/obj/${name}")
@@ -38,7 +38,7 @@ macro(add_deglib_test_no_ctest name source)
3838
add_executable(${name} ${TEST_MAIN} ${source})
3939
target_include_directories(${name} PRIVATE ${DEGLIB_INCLUDE})
4040
target_include_directories(${name} PRIVATE ${GTEST_INCLUDE})
41-
target_include_directories(${name} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/regression)
41+
target_include_directories(${name} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
4242
target_sources(${name} PRIVATE ${GTEST_SOURCE})
4343
target_link_libraries(${name} PRIVATE compile-options)
4444
set_target_properties(${name} PROPERTIES INTERMEDIATE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/obj/${name}")
@@ -49,14 +49,10 @@ add_deglib_test(test_cpu_features src/test_cpu_features.cpp)
4949

5050
# Integration tests (fast, recall-only, registered with CTest for CI)
5151
add_deglib_integration_test(test_builder_integration src/integration/test_builder_integration.cpp)
52-
add_deglib_integration_test(test_distance_integration src/integration/test_distance_integration.cpp)
52+
add_deglib_integration_test(test_distances_integration src/integration/test_distances_integration.cpp)
5353

5454
# Regression tests (100k vectors, performance benchmarking — built but NOT registered with CTest)
55-
add_deglib_test_no_ctest(test_dataset_determinism src/regression/test_dataset_determinism.cpp)
56-
add_deglib_test_no_ctest(test_l2_regression src/regression/metric/test_l2_regression.cpp)
57-
add_deglib_test_no_ctest(test_innerproduct_regression src/regression/metric/test_innerproduct_regression.cpp)
58-
add_deglib_test_no_ctest(test_l2_uint8_regression src/regression/metric/test_l2_uint8_regression.cpp)
59-
add_deglib_test_no_ctest(test_l2_builder_regression src/regression/builder/test_l2_builder_regression.cpp)
55+
add_deglib_test_no_ctest(test_builder_regression src/regression/test_builder_regression.cpp)
6056

6157
# Meta target to build all regression tests
62-
add_custom_target(test_regression DEPENDS test_dataset_determinism test_l2_regression test_innerproduct_regression test_l2_uint8_regression test_l2_builder_regression)
58+
add_custom_target(test_regression DEPENDS test_builder_regression)

cpp/test/src/integration/test_builder_integration.h renamed to cpp/test/src/common/test_helpers.h

Lines changed: 156 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,11 @@
1414
#include <vector>
1515

1616
// ============================================================================
17-
// Integration Test Shared Utilities
17+
// Shared Test Utilities
1818
// ============================================================================
19-
// Centralized helpers for fast integration tests operating on 10,000 base
20-
// vectors and 100 queries. These are adapted from test_regression.h but
21-
// strip out QPS / build-time assertions so that only recall correctness is
22-
// verified. All dataset generation uses the same bit-exact 32-bit PRNG so
23-
// results are reproducible across platforms.
19+
// Centralized helpers for both integration and regression tests.
20+
// All dataset generation uses a bit-exact 32-bit PRNG so results are
21+
// reproducible across MSVC, GCC, and Clang on all CPU architectures.
2422
// ============================================================================
2523

2624
// ---------------------------------------------------------------------------
@@ -35,10 +33,20 @@ inline static uint32_t deglib_prng_next(uint32_t& state) {
3533
return state = x;
3634
}
3735

36+
inline static float deglib_prng_float(uint32_t& state, float min_val, float max_val) {
37+
uint32_t val = deglib_prng_next(state) >> 8; // 24-bit integer
38+
float u = static_cast<float>(val) / 16777215.0f;
39+
float range = max_val - min_val;
40+
return min_val + u * range;
41+
}
42+
3843
// ---------------------------------------------------------------------------
3944
// Dataset generation (float, for L2 and InnerProduct)
4045
// ---------------------------------------------------------------------------
4146

47+
// Generate cross-platform deterministic clustered dataset (Gaussian Mixture with fixed seed)
48+
// Uses pure 32-bit integer arithmetic and exact integer-to-float conversion to guarantee
49+
// 100% bit-exact float vectors across MSVC, GCC, and Clang on all CPU architectures.
4250
inline static void generate_synthetic_clustered_dataset(size_t count, size_t dim, std::vector<float>& base,
4351
std::vector<float>& query, size_t query_count,
4452
size_t num_clusters = 20)
@@ -85,6 +93,7 @@ inline static void generate_synthetic_clustered_dataset(size_t count, size_t dim
8593
// Dataset generation (uint8, for L2_Uint8)
8694
// ---------------------------------------------------------------------------
8795

96+
// Generate cross-platform deterministic uint8 clustered dataset
8897
inline static void generate_synthetic_clustered_dataset_uint8(size_t count, size_t dim, std::vector<uint8_t>& base,
8998
std::vector<uint8_t>& query, size_t query_count,
9099
size_t num_clusters = 20)
@@ -133,6 +142,8 @@ inline static void generate_synthetic_clustered_dataset_uint8(size_t count, size
133142
// Groundtruth computation
134143
// ---------------------------------------------------------------------------
135144

145+
// Compute exact brute-force groundtruth for top-K neighbors using a custom distance evaluator.
146+
// Works for both float and uint8_t element types via the ElemType template parameter.
136147
template <typename ElemType, typename DistFunc>
137148
inline static std::vector<std::vector<uint32_t>> compute_groundtruth_custom(const std::vector<ElemType>& base, size_t base_count,
138149
const std::vector<ElemType>& query, size_t query_count,
@@ -163,6 +174,9 @@ inline static std::vector<std::vector<uint32_t>> compute_groundtruth_custom(cons
163174
return gt;
164175
}
165176

177+
// Compute exact brute-force L2 groundtruth for top-K neighbors.
178+
// Uses the scalar L2Float::compare() implementation from deglib to ensure
179+
// the ground-truth distances match the actual distance computation exactly.
166180
inline static std::vector<std::vector<uint32_t>> compute_groundtruth_l2(const std::vector<float>& base, size_t base_count,
167181
const std::vector<float>& query, size_t query_count,
168182
size_t dim, uint32_t k)
@@ -174,6 +188,9 @@ inline static std::vector<std::vector<uint32_t>> compute_groundtruth_l2(const st
174188
});
175189
}
176190

191+
// Compute exact brute-force InnerProduct groundtruth for top-K neighbors (distance = 1 - dot_product).
192+
// Uses the scalar InnerProductFloat::compare() implementation from deglib to ensure
193+
// the ground-truth distances match the actual distance computation exactly.
177194
inline static std::vector<std::vector<uint32_t>> compute_groundtruth_innerproduct(const std::vector<float>& base, size_t base_count,
178195
const std::vector<float>& query, size_t query_count,
179196
size_t dim, uint32_t k)
@@ -185,6 +202,9 @@ inline static std::vector<std::vector<uint32_t>> compute_groundtruth_innerproduc
185202
});
186203
}
187204

205+
// Compute exact brute-force L2 groundtruth for uint8 vectors.
206+
// Uses the scalar L2Uint8::compare() from deglib to ensure the ground-truth
207+
// distances match the actual distance computation exactly.
188208
inline static std::vector<std::vector<uint32_t>> compute_groundtruth_l2_uint8(const std::vector<uint8_t>& base, size_t base_count,
189209
const std::vector<uint8_t>& query, size_t query_count,
190210
size_t dim, uint32_t k)
@@ -240,7 +260,7 @@ inline static void check_distance_recall(const char* name, const std::vector<Ele
240260
}
241261

242262
// ---------------------------------------------------------------------------
243-
// Graph builder + search recall runner (no QPS / build-time checks)
263+
// Graph builder + search recall runner (integration: no QPS / build-time checks)
244264
// ---------------------------------------------------------------------------
245265

246266
inline static void run_integration_test(const char* name, deglib::Metric metric, double min_recall,
@@ -426,10 +446,137 @@ inline static void run_builder_integration_test(const char* name, deglib::Metric
426446
}
427447
}
428448

449+
// ---------------------------------------------------------------------------
450+
// Universal regression benchmark runner function for any metric
451+
// num_runs: number of measured search runs (averaged for QPS/recall).
452+
// Higher values extend the total search measurement window, reducing QPS noise.
453+
// optimization_target: controls the graph build strategy (LowLID, HighLID, StreamingData).
454+
// ---------------------------------------------------------------------------
455+
456+
inline static void run_regression_test(const char* name, deglib::Metric metric, double min_qps, double max_build_secs,
457+
double min_recall, const void* base_data,
458+
const void* query_data, size_t base_count, size_t query_count,
459+
size_t dim, const std::vector<std::vector<uint32_t>>& gt_data,
460+
std::optional<deglib::DistanceVariant> dist_variant = std::nullopt,
461+
size_t num_runs = 5,
462+
deglib::builder::OptimizationTarget optimization_target = deglib::builder::OptimizationTarget::LowLID)
463+
{
464+
const uint32_t search_k = 10;
465+
const float search_eps = 0.05f;
466+
467+
const uint32_t edges_per_vertex = 32;
468+
const uint8_t extend_k = static_cast<uint8_t>(edges_per_vertex);
469+
const float extend_eps = 0.1f;
470+
const uint8_t improve_k = 0;
471+
const float improve_eps = 0.0f;
472+
const uint8_t max_path_length = 5;
473+
const uint32_t swap_tries = 0;
474+
const uint32_t additional_swap_tries = 0;
475+
const uint32_t thread_count = 1;
476+
477+
// Compute byte size per vector based on metric type (0x10 flag indicates 8-bit integer)
478+
const size_t feature_bytes = (static_cast<int>(metric) & 0x10) ? dim * sizeof(uint8_t) : dim * sizeof(float);
479+
480+
// Build DEG Graph using the specified metric feature space
481+
const deglib::FloatSpace feature_space = dist_variant.has_value()
482+
? deglib::FloatSpace(dim, metric, dist_variant.value())
483+
: deglib::FloatSpace(dim, metric);
484+
485+
deglib::graph::SizeBoundedGraph graph(static_cast<uint32_t>(base_count), edges_per_vertex,
486+
std::move(feature_space));
487+
488+
std::mt19937 rng(1337);
489+
deglib::builder::EvenRegularGraphBuilder builder(graph, rng, optimization_target, extend_k, extend_eps, improve_k,
490+
improve_eps, max_path_length, swap_tries, additional_swap_tries);
491+
builder.setThreadCount(thread_count);
492+
auto t_build_start = std::chrono::high_resolution_clock::now();
493+
494+
const std::byte* base_bytes = reinterpret_cast<const std::byte*>(base_data);
495+
for (size_t i = 0; i < base_count; ++i)
496+
{
497+
const std::byte* ptr = base_bytes + i * feature_bytes;
498+
std::vector<std::byte> feat_vec(ptr, ptr + feature_bytes);
499+
builder.addEntry(static_cast<uint32_t>(i), std::move(feat_vec));
500+
}
501+
502+
auto build_callback = [](deglib::builder::BuilderStatus& status) {};
503+
builder.build(build_callback);
504+
505+
auto t_build_end = std::chrono::high_resolution_clock::now();
506+
double build_secs = std::chrono::duration<double>(t_build_end - t_build_start).count();
507+
508+
auto entry_vertex_indices = graph.getEntryVertexIndices();
509+
const std::byte* query_bytes = reinterpret_cast<const std::byte*>(query_data);
510+
511+
auto run_search = [&]() -> std::pair<double, double>
512+
{
513+
size_t total_correct = 0;
514+
auto t_search_start = std::chrono::high_resolution_clock::now();
515+
516+
for (size_t q = 0; q < query_count; ++q)
517+
{
518+
const std::byte* q_ptr = query_bytes + q * feature_bytes;
519+
auto result = graph.search(entry_vertex_indices, q_ptr, search_eps, search_k, nullptr, 0);
520+
521+
std::unordered_set<uint32_t> gt_set;
522+
if (!gt_data.empty() && q < gt_data.size())
523+
{
524+
size_t eval_k = std::min(static_cast<size_t>(search_k), gt_data[q].size());
525+
for (size_t i = 0; i < eval_k; ++i)
526+
{
527+
gt_set.insert(gt_data[q][i]);
528+
}
529+
}
530+
531+
while (!result.empty())
532+
{
533+
auto top_item = result.top();
534+
result.pop();
535+
uint32_t ext_label = graph.getExternalLabel(top_item.getInternalIndex());
536+
if (gt_set.count(ext_label))
537+
{
538+
total_correct++;
539+
}
540+
}
541+
}
542+
543+
auto t_search_end = std::chrono::high_resolution_clock::now();
544+
double search_secs = std::chrono::duration<double>(t_search_end - t_search_start).count();
545+
double qps = static_cast<double>(query_count) / search_secs;
546+
double recall = static_cast<double>(total_correct) / static_cast<double>(query_count * search_k);
547+
return {qps, recall};
548+
};
549+
550+
// Warm-up run
551+
run_search();
552+
553+
// Measured average runs — higher num_runs extends the measurement window,
554+
// reducing QPS noise from OS jitter and CPU power-state transitions.
555+
double total_qps = 0.0;
556+
double total_recall = 0.0;
557+
for (size_t r = 0; r < num_runs; ++r)
558+
{
559+
auto [qps, recall] = run_search();
560+
total_qps += qps;
561+
total_recall += recall;
562+
}
563+
double qps = total_qps / num_runs;
564+
double recall = total_recall / num_runs;
565+
566+
std::cout << "[" << name << "] build_secs=" << build_secs << " qps=" << qps << " recall=" << recall << std::endl;
567+
568+
EXPECT_GE(recall + 1e-5, min_recall);
569+
if (std::getenv("SKIP_PERFORMANCE_TESTS") == nullptr) {
570+
EXPECT_GT(qps, min_qps);
571+
EXPECT_LE(build_secs, max_build_secs);
572+
}
573+
}
574+
429575
// ---------------------------------------------------------------------------
430576
// Checksums for dataset determinism verification
431577
// ---------------------------------------------------------------------------
432578

579+
// FNV-1a 64-bit hash for byte buffers
433580
inline static uint64_t fnv1a_64(const void* data, size_t bytes) {
434581
const uint8_t* ptr = static_cast<const uint8_t*>(data);
435582
uint64_t hash = 14695981039346656037ULL;
@@ -440,10 +587,12 @@ inline static uint64_t fnv1a_64(const void* data, size_t bytes) {
440587
return hash;
441588
}
442589

590+
// Compute checksum of a float vector
443591
inline static uint64_t float_vector_checksum(const std::vector<float>& vec) {
444592
return fnv1a_64(vec.data(), vec.size() * sizeof(float));
445593
}
446594

595+
// Compute checksum of groundtruth 2D vector
447596
inline static uint64_t groundtruth_checksum(const std::vector<std::vector<uint32_t>>& gt) {
448597
uint64_t hash = 14695981039346656037ULL;
449598
for (const auto& row : gt) {

cpp/test/src/integration/test_builder_integration.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#include "test_builder_integration.h"
1+
#include "common/test_helpers.h"
22

33
// ============================================================================
44
// Builder & Search Recall Integration Tests

cpp/test/src/integration/test_distance_integration.cpp renamed to cpp/test/src/integration/test_distances_integration.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#include "test_builder_integration.h"
1+
#include "common/test_helpers.h"
22

33
// ============================================================================
44
// Distance Calculation Recall Integration Tests

0 commit comments

Comments
 (0)