diff --git a/.github/scripts/test_ci_workflow_contracts.py b/.github/scripts/test_ci_workflow_contracts.py index a2d1f417..bb1036dc 100644 --- a/.github/scripts/test_ci_workflow_contracts.py +++ b/.github/scripts/test_ci_workflow_contracts.py @@ -17,6 +17,26 @@ class WorkflowContractsTest(unittest.TestCase): + def test_ctest_invocations_reject_empty_selections(self): + workflow = (ROOT / ".github/workflows/test.yaml").read_text() + commands = re.findall(r"\bctest\s+[^\n]+", workflow) + self.assertTrue(commands) + for command in commands: + with self.subTest(command=command): + self.assertEqual( + re.findall(r"--no-tests(?:=|\s+)(\w+)", command), ["error"] + ) + + with tempfile.TemporaryDirectory() as directory: + result = subprocess.run( + ["ctest", "--test-dir", directory, "--no-tests=error"], + capture_output=True, + text=True, + check=False, + ) + self.assertNotEqual(result.returncode, 0) + self.assertIn("No tests were found", result.stdout + result.stderr) + def test_wheels_preserve_abi_coverage_and_release_gates(self): workflow = (ROOT / ".github/workflows/release.yml").read_text() config = tomllib.loads((ROOT / "pyproject.toml").read_text()) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 9c51a716..c29d7133 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -152,14 +152,14 @@ jobs: -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_PREFIX_PATH="$PWD/rabitqlib-native-install" cmake --build build-native-consumer - ctest --test-dir build-native-consumer --output-on-failure + ctest --test-dir build-native-consumer --output-on-failure --no-tests=error - name: Test add_subdirectory isolation run: | cmake -S tests/consumer -B build-source-consumer -G Ninja \ -DCMAKE_BUILD_TYPE=Release -DRABITQ_SOURCE_DIR="$PWD" \ -DRABITQ_ENABLE_NATIVE_OPTIMIZATION=ON cmake --build build-source-consumer - ctest --test-dir build-source-consumer --output-on-failure + ctest --test-dir build-source-consumer --output-on-failure --no-tests=error clang: name: Clang 23 (OpenMP ${{ matrix.openmp }}) @@ -256,14 +256,14 @@ jobs: - name: Build run: cmake --build build-windows --config Release --parallel 2 - name: Test - run: ctest --test-dir build-windows -C Release --output-on-failure + run: ctest --test-dir build-windows -C Release --output-on-failure --no-tests=error - name: Test installed CMake package and header requirements run: | cmake --install build-windows --config Release --prefix "$PWD/rabitqlib-install" cmake -S tests/consumer -B build-consumer -G "Visual Studio 18 2026" -A x64 ` "-DCMAKE_PREFIX_PATH=$PWD/rabitqlib-install" cmake --build build-consumer --config Release --parallel 2 - ctest --test-dir build-consumer -C Release --output-on-failure + ctest --test-dir build-consumer -C Release --output-on-failure --no-tests=error sanitizers: needs: changes @@ -305,7 +305,7 @@ jobs: -DCMAKE_PREFIX_PATH="$PWD/rabitqlib-sanitized-install" \ -DRABITQ_EXPECT_SANITIZERS=ON cmake --build build-sanitized-consumer - ctest --test-dir build-sanitized-consumer --output-on-failure + ctest --test-dir build-sanitized-consumer --output-on-failure --no-tests=error downstream-consumer: needs: changes @@ -330,7 +330,7 @@ jobs: -DCMAKE_INSTALL_PREFIX="$PWD/rabitqlib-install" cmake --build build-install - name: Test portable Release build - run: ctest --test-dir build-install --output-on-failure + run: ctest --test-dir build-install --output-on-failure --no-tests=error - name: Download pinned Intel SDE run: | curl --fail --location --retry 3 --max-time 120 \ @@ -365,7 +365,7 @@ jobs: -DCMAKE_PREFIX_PATH="$PWD/rabitqlib-install" cmake --build build-consumer - name: Test downstream consumer - run: ctest --test-dir build-consumer --output-on-failure + run: ctest --test-dir build-consumer --output-on-failure --no-tests=error core-consumer: name: Core and FastScan consumers (${{ matrix.os }}, OpenMP ${{ matrix.openmp }}${{ matrix.component && format(', {0}', matrix.component) || '' }}) @@ -483,14 +483,14 @@ jobs: - name: Build and run C++ tests run: | cmake --build build --parallel 2 - ctest --test-dir build --output-on-failure + ctest --test-dir build --output-on-failure --no-tests=error - name: Test installed CMake package run: | cmake --install build --prefix "$RUNNER_TEMP/rabitq-install" cmake -S tests/consumer -B build-consumer -G Ninja \ -DCMAKE_PREFIX_PATH="$RUNNER_TEMP/rabitq-install" cmake --build build-consumer --parallel 2 - ctest --test-dir build-consumer --output-on-failure + ctest --test-dir build-consumer --output-on-failure --no-tests=error macos-arm64: name: Native Apple Silicon C++ @@ -515,7 +515,7 @@ jobs: - name: Build and run C++ reference and index tests run: | cmake --build build --parallel 3 - ctest --test-dir build --output-on-failure + ctest --test-dir build --output-on-failure --no-tests=error - name: Test installed CMake package run: | cmake --install build --prefix "$RUNNER_TEMP/rabitq-install" @@ -523,4 +523,4 @@ jobs: -DCMAKE_PREFIX_PATH="$RUNNER_TEMP/rabitq-install" \ -DOpenMP_ROOT="$(brew --prefix libomp)" cmake --build build-consumer --parallel 3 - ctest --test-dir build-consumer --output-on-failure + ctest --test-dir build-consumer --output-on-failure --no-tests=error diff --git a/tests/core_consumer/core_static_init.cpp b/tests/core_consumer/core_static_init.cpp index 6084c0bd..c70b435d 100644 --- a/tests/core_consumer/core_static_init.cpp +++ b/tests/core_consumer/core_static_init.cpp @@ -14,13 +14,22 @@ namespace { const bool kBeforeMain = [] { namespace api = rabitqlib::quant::v1; std::array input{}, rotated{}, query{}; + double input_norm_sqr = 0; for (size_t i = 0; i < input.size(); ++i) { input[i] = static_cast(i % 11) - 5.0F; + input_norm_sqr += static_cast(input[i]) * input[i]; } rabitqlib::rotator_impl::FhtKacRotator rotator(64, 64, 42); rotator.rotate(input.data(), rotated.data()); - query = rotated; + // A half-length collinear query gives known, nonzero results for both metrics. + for (size_t i = 0; i < query.size(); ++i) { + query[i] = rotated[i] * 0.5F; + } for (const auto metric : {rabitqlib::METRIC_L2, rabitqlib::METRIC_IP}) { + const double expected = metric == rabitqlib::METRIC_L2 ? 0.25 * input_norm_sqr + : 1 - 0.5 * input_norm_sqr; + // Reference the original coordinates, allowing float rotation/scoring roundoff. + const double tolerance = 2e-5 * (1 + input_norm_sqr); for (size_t bits = 1; bits <= 9; ++bits) { const api::Parameters params{64, bits, metric}; const auto req = api::requirements(params, api::CodeLayout::SplitPacked); @@ -46,7 +55,7 @@ const bool kBeforeMain = [] { {extra.data(), extra.size()}}, factors ); - if (!std::isfinite(distance)) { + if (!std::isfinite(distance) || std::abs(distance - expected) > tolerance) { return false; } } diff --git a/tests/dispatch/modules_static_init.cpp b/tests/dispatch/modules_static_init.cpp index 633c2d5a..b3762ebb 100644 --- a/tests/dispatch/modules_static_init.cpp +++ b/tests/dispatch/modules_static_init.cpp @@ -1,4 +1,5 @@ #include +#include #include #include "rabitqlib/fastscan/fastscan.hpp" @@ -10,18 +11,44 @@ namespace { const bool kBeforeMain = [] { std::array query{}; + for (size_t i = 0; i < query.size(); ++i) { + query[i] = static_cast(i % 4 + 1); + } std::array lut{}; + lut.fill(-1.0F); rabitqlib::fastscan::pack_lut(64, query.data(), lut.data()); + for (size_t i = 0; i < lut.size(); ++i) { + // The first coordinate occupies the high bit of each four-bit code. + float expected = 0; + for (size_t bit = 0; bit < 4; ++bit) { + if ((i % 16) & (size_t{1} << bit)) { + expected += static_cast(4 - bit); + } + } + if (lut[i] != expected) { + return false; + } + } std::array integer_lut{}; + // Equal nonzero bytes make the packed result independent of the HACC layout. + integer_lut.fill(0x0101); std::array packed_lut{}; + packed_lut.fill(0xAB); std::array codes{}; std::array scores{}; + scores.fill(-1); rabitqlib::fastscan::transfer_lut_hacc(integer_lut.data(), 64, packed_lut.data()); + for (const auto value : packed_lut) { + if (value != 1) { + return false; + } + } rabitqlib::fastscan::accumulate_hacc( codes.data(), packed_lut.data(), scores.data(), 64 ); for (const auto score : scores) { - if (score != 0) { + // Sixteen four-coordinate tables, each contributing 0x0101. + if (score != 16 * 0x0101) { return false; } }