feat(test): enable parallel tests (#384)

- Set unique WORKING_DIRECTORY per test binary via cc_test()/cuda_test() to prevent filesystem path conflicts when running tests in parallel. Each test runs in ${CMAKE_BINARY_DIR}/test_tmp/${test_name}/.

- Enable parallel ctest execution in the unittest target with ProcessorCount-based --parallel flag (defaults to NPROC - 1).

- Set TEST_BINARY_DIR environment variable for crash recovery tests so they can locate helper binaries from isolated working directories.

- Update LocateDataGenerator() and LocateOptimizeGenerator() to search TEST_BINARY_DIR and TEST_BINARY_DIR/bin for helper executables.
This commit is contained in:
Cuiys 2026-05-12 23:14:17 +08:00 committed by GitHub
parent d0489da401
commit 42958caef5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 82 additions and 69 deletions

View File

@ -93,6 +93,7 @@ jobs:
cmake==3.30.0 \
ninja==1.11.1 \
pytest \
pytest-xdist \
scikit-build-core \
setuptools_scm
shell: bash

View File

@ -64,6 +64,7 @@ jobs:
cmake==3.30.0 `
ninja==1.11.1 `
pytest `
pytest-xdist `
scikit-build-core `
setuptools_scm
shell: powershell

View File

@ -59,6 +59,7 @@ jobs:
ninja==1.11.1 \
pytest \
pytest-cov \
pytest-xdist \
scikit-build-core \
setuptools_scm
shell: bash

View File

@ -318,10 +318,20 @@ if(NOT TARGET unittest)
# iOS: build-only target; tests are run on simulator separately
add_custom_target(unittest)
else()
include(ProcessorCount)
ProcessorCount(NPROC)
if(NPROC EQUAL 0)
set(NPROC 1)
endif()
math(EXPR PARALLEL_JOBS "${NPROC} - 1")
if(PARALLEL_JOBS LESS 1)
set(PARALLEL_JOBS 1)
endif()
add_custom_target(
unittest
COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure
--build-config $<CONFIGURATION>
--parallel ${PARALLEL_JOBS}
)
endif()
endif()
@ -1110,16 +1120,18 @@ function(cc_test)
"${CC_ARGS_UNPARSED_ARGUMENTS}"
)
add_dependencies(unittest ${CC_ARGS_NAME})
set(TEST_WORKING_DIR "${CMAKE_BINARY_DIR}/test_tmp/${CC_ARGS_NAME}")
file(MAKE_DIRECTORY "${TEST_WORKING_DIR}")
add_custom_target(
unittest.${CC_ARGS_NAME}
COMMAND $<TARGET_FILE:${CC_ARGS_NAME}> "${CC_ARGS_ARGS}"
WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
WORKING_DIRECTORY ${TEST_WORKING_DIR}
DEPENDS ${CC_ARGS_NAME}
)
add_test(
NAME ${CC_ARGS_NAME}
COMMAND $<TARGET_FILE:${CC_ARGS_NAME}> "${CC_ARGS_ARGS}"
WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
WORKING_DIRECTORY ${TEST_WORKING_DIR}
)
endfunction()
@ -1925,16 +1937,18 @@ function(cuda_test)
"${CUDA_ARGS_UNPARSED_ARGUMENTS}"
)
add_dependencies(unittest ${CUDA_ARGS_NAME})
set(TEST_WORKING_DIR "${CMAKE_BINARY_DIR}/test_tmp/${CUDA_ARGS_NAME}")
file(MAKE_DIRECTORY "${TEST_WORKING_DIR}")
add_custom_target(
unittest.${CUDA_ARGS_NAME}
COMMAND $<TARGET_FILE:${CUDA_ARGS_NAME}> "${CUDA_ARGS_ARGS}"
WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
WORKING_DIRECTORY ${TEST_WORKING_DIR}
DEPENDS ${CUDA_ARGS_NAME}
)
add_test(
NAME ${CUDA_ARGS_NAME}
COMMAND $<TARGET_FILE:${CUDA_ARGS_NAME}> "${CUDA_ARGS_ARGS}"
WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
WORKING_DIRECTORY ${TEST_WORKING_DIR}
)
endfunction()

View File

@ -51,6 +51,7 @@ test = [
"pytest >=8.0",
"pytest-cov >=4.1",
"pytest-mock >=3.12",
"pytest-xdist >=3.5",
"cibuildwheel == 3.4.0",
]
docs = [
@ -142,6 +143,7 @@ addopts = [
"--strict-markers",
"--strict-config",
"--tb=short",
"-n=auto",
]
xfail_strict = true
log_cli_level = "INFO"
@ -169,7 +171,7 @@ build = [
"cp314-*",
]
build-frontend = "build"
test-requires = ["pytest", "numpy"]
test-requires = ["pytest", "pytest-xdist", "numpy"]
test-command = "cd {project} && pytest python/tests -v --tb=short"
build-verbosity = 1

View File

@ -77,5 +77,8 @@ foreach(CC_SRCS ${ALL_TEST_SRCS})
)
add_dependencies(${CC_TARGET} data_generator)
add_dependencies(${CC_TARGET} collection_optimizer)
set_tests_properties(${CC_TARGET} PROPERTIES
ENVIRONMENT "TEST_BINARY_DIR=${PROJECT_BINARY_DIR}"
)
cc_test_suite(zvec_crash_recovery ${CC_TARGET})
endforeach()

View File

@ -46,38 +46,7 @@ const int num_batches{1000};
static std::string LocateOptimizeGenerator() {
namespace fs = std::filesystem;
std::cout << "Current path: " << fs::current_path() << std::endl;
const std::string base_name = "collection_optimizer";
std::vector<std::string> candidates;
const std::vector<std::string> search_paths = {"./", "./bin/"};
for (const auto &p : search_paths) {
candidates.push_back(p);
}
// TODO(windows): unify _WIN32/_WIN64/MSCV_VER
#ifdef _WIN32
for (const auto &p : search_paths) {
candidates.push_back(p + "Debug/");
candidates.push_back(p + "Release/");
}
#endif
for (auto &p : candidates) {
p += base_name;
#ifdef _WIN32
p += ".exe";
#endif
}
for (const auto &p : candidates) {
if (fs::exists(p)) {
return fs::canonical(p).string();
}
}
throw std::runtime_error("collection_optimizer binary not found");
return LocateBinary("collection_optimizer");
}

View File

@ -16,6 +16,13 @@
#pragma once
#include <cstdlib>
#include <filesystem>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
#include <zvec/db/collection.h>
#include <zvec/db/doc.h>
@ -149,4 +156,50 @@ inline Doc CreateTestDoc(uint64_t doc_id, int version) {
}
/**
* @brief Locate a binary by name, searching common paths and TEST_BINARY_DIR.
*
* @param binary_name The base name of the binary (e.g. "data_generator")
* @return std::string The canonical path to the found binary
* @throws std::runtime_error if the binary is not found
*/
inline std::string LocateBinary(const std::string &binary_name) {
namespace fs = std::filesystem;
std::cout << "Current path: " << fs::current_path() << std::endl;
std::vector<std::string> candidates;
const std::vector<std::string> search_paths = {"./", "./bin/"};
for (const auto &p : search_paths) {
candidates.push_back(p);
}
#ifdef _WIN32
for (const auto &p : search_paths) {
candidates.push_back(p + "Debug/");
candidates.push_back(p + "Release/");
}
#endif
const char *test_binary_dir = std::getenv("TEST_BINARY_DIR");
if (test_binary_dir != nullptr) {
candidates.push_back(std::string(test_binary_dir) + "/");
candidates.push_back(std::string(test_binary_dir) + "/bin/");
}
for (auto &p : candidates) {
p += binary_name;
#ifdef _WIN32
p += ".exe";
#endif
}
for (const auto &p : candidates) {
if (fs::exists(p)) {
return fs::canonical(p).string();
}
}
throw std::runtime_error(binary_name + " binary not found");
}
} // namespace zvec

View File

@ -44,38 +44,7 @@ const zvec::CollectionOptions options_{false, true, 256 * 1024};
static std::string LocateDataGenerator() {
namespace fs = std::filesystem;
std::cout << "Current path: " << fs::current_path() << std::endl;
const std::string base_name = "data_generator";
std::vector<std::string> candidates;
// Define potential search locations relative to the current working directory
const std::vector<std::string> search_paths = {"./", "./bin/"};
for (const auto &p : search_paths) {
candidates.push_back(p);
}
#ifdef _WIN32
for (const auto &p : search_paths) {
candidates.push_back(p + "Debug/");
candidates.push_back(p + "Release/");
}
#endif
for (auto &p : candidates) {
p += base_name;
#ifdef _WIN32
p += ".exe"; // Append .exe suffix for Windows compatibility
#endif
}
for (const auto &p : candidates) {
if (fs::exists(p)) {
return fs::canonical(p).string();
}
}
throw std::runtime_error("data_generator binary not found");
return LocateBinary("data_generator");
}