diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index b89ec823..8ac9e218 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -5,6 +5,7 @@ FROM mcr.microsoft.com/devcontainers/base:ubuntu26.04 # install needed packages RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ && apt-get -y install \ + catch2 \ clang \ clang-tidy \ clang-format \ @@ -19,7 +20,6 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ python3 \ openmpi-bin \ libboost-dev \ - libboost-test-dev \ libhwloc-dev \ libmsgpack-cxx-dev \ libopenmpi-dev \ diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 51fc2d14..8bb8ece8 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -29,7 +29,7 @@ jobs: - name: Install dependencies from APT run: | sudo apt-get update - sudo apt-get install -y just libopenmpi-dev openmpi-bin libboost-dev libboost-test-dev libmsgpack-cxx-dev libhwloc-dev + sudo apt-get install -y just libopenmpi-dev openmpi-bin libboost-dev libmsgpack-cxx-dev libhwloc-dev catch2 - name: Install the latest version of uv uses: astral-sh/setup-uv@v10.0.1 diff --git a/.github/workflows/docpages.yml b/.github/workflows/docpages.yml index a874af3c..a44a6546 100644 --- a/.github/workflows/docpages.yml +++ b/.github/workflows/docpages.yml @@ -52,7 +52,7 @@ jobs: - name: Install dependencies from APT run: | sudo apt-get update - sudo apt-get install -y just libopenmpi-dev openmpi-bin libboost-dev libboost-test-dev libmsgpack-cxx-dev libhwloc-dev + sudo apt-get install -y just libopenmpi-dev openmpi-bin libboost-dev libmsgpack-cxx-dev libhwloc-dev catch2 - name: Install the latest version of uv uses: astral-sh/setup-uv@v10.0.1 diff --git a/.github/workflows/qa-analysis.yml b/.github/workflows/qa-analysis.yml index cf5c68a9..9544bac0 100644 --- a/.github/workflows/qa-analysis.yml +++ b/.github/workflows/qa-analysis.yml @@ -70,7 +70,7 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y libopenmpi-dev openmpi-bin libboost-dev libboost-test-dev libmsgpack-cxx-dev libhwloc-dev + sudo apt-get install -y libopenmpi-dev openmpi-bin libboost-dev libmsgpack-cxx-dev libhwloc-dev catch2 - name: Install the latest version of uv uses: astral-sh/setup-uv@v10.0.1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 326037b7..7b924408 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -69,7 +69,7 @@ jobs: brew install boost open-mpi msgpack-cxx hwloc else sudo apt-get update - packages="libopenmpi-dev openmpi-bin libboost-dev libboost-test-dev libmsgpack-cxx-dev libhwloc-dev" + packages="libopenmpi-dev openmpi-bin libboost-dev libmsgpack-cxx-dev libhwloc-dev catch2" if [[ "${{ matrix.compiler }}" == "clang++-18" ]]; then packages="$packages clang-18" fi diff --git a/AGENTS.md b/AGENTS.md index 35bcd09e..5d81766f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -149,7 +149,8 @@ mp = MajoranaPropagator(operator, initial_state, cutoff=4) - **nanobind**: Modern Python-C++ binding (prefer over pybind11) - **scikit-build-core**: Modern build system replacing setuptools - **uv**: Package management -- **Boost**: Used for various utilities (unordered_map, unit tests) +- **Boost**: Used for production C++ utilities such as unordered containers +- **Catch2 v3**: C++ unit-test framework - **msgpack**: Serialization of the test-data fixtures only (`tests/data/*.msgpack`); consumed by the Python test loaders and the C++ test suite, not by the shipped library - **hwloc**: CPU topology discovery and thread binding for partition placement (`CpuTopology.cpp`). Required system library (`libhwloc-dev` on Debian/Ubuntu, `hwloc` on Homebrew). Requires `pkg-config` so CMake can locate `hwloc`. Bundled into wheels automatically by auditwheel/delocate. - **MPI**: For distributed parallelization diff --git a/README.md b/README.md index 14d0b660..8ee1a89e 100644 --- a/README.md +++ b/README.md @@ -100,13 +100,16 @@ uv sync --all-extras -v uv sync --all-extras -v --config-settings=cmake.define.monoprop_ENABLE_MPI=ON ``` -C++ unit-test build: +C++ unit-test build (Catch2 v3, registered through CTest): ```bash uv sync --all-extras -v ctest --test-dir build/editable/Release ``` +Release builds compile the test sources at `-O1` to reduce template-heavy build +time while keeping the library at `-O3`. + Full instructions — prerequisites, MPI options, and running the example executable — are in the [building guide](https://docs.monoprop.algorithmiq.tech/building). In particular, from-source builds require `hwloc` and `pkg-config` so CMake can diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 9a34fe1a..2cb355f2 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -1,7 +1,15 @@ -find_package(Boost 1.85 COMPONENTS unit_test_framework REQUIRED) - include(${PROJECT_SOURCE_DIR}/cmake/CPM.cmake) +cpmfindpackage( + NAME Catch2 + VERSION 3 + GIT_REPOSITORY "https://github.com/catchorg/Catch2.git" + GIT_TAG "v3.15.3" + OPTIONS "CATCH_INSTALL_DOCS OFF" + SYSTEM YES + EXCLUDE_FROM_ALL YES +) + cpmaddpackage( NAME "msgpack-cxx" GIT_REPOSITORY "https://github.com/msgpack/msgpack-c" @@ -26,12 +34,7 @@ file( add_executable(monoprop_unit_tests.x ${_tests_cpps}) -target_compile_definitions( - monoprop_unit_tests.x - PUBLIC - BOOST_TEST_DYN_LINK - BOOST_TEST_NO_MAIN -) +target_compile_options(monoprop_unit_tests.x PRIVATE $<$:-O1>) target_include_directories( monoprop_unit_tests.x @@ -44,14 +47,19 @@ target_link_libraries( monoprop_unit_tests.x PRIVATE monoprop-objs - Boost::unit_test_framework + Catch2::Catch2 msgpack-cxx PkgConfig::HWLOC ) -include(${CMAKE_CURRENT_LIST_DIR}/boost-test.cmake) +if(Catch2_SOURCE_DIR) + list(APPEND CMAKE_MODULE_PATH "${Catch2_SOURCE_DIR}/extras") +else() + list(APPEND CMAKE_MODULE_PATH "${Catch2_DIR}") +endif() +include(Catch) -# CTest launches each Boost case as a world-size-1 process, so excluding fabric components cuts +# CTest launches each Catch2 case as a world-size-1 process, so excluding fabric components cuts # MPI_Init from 2.03 s to 0.61 s without affecting communication; real MPI tests must keep the # full component set. # @@ -71,14 +79,76 @@ if(monoprop_TEST_EXCLUDE_MPI_FABRIC AND monoprop_ENABLE_MPI) ) endif() -# Automatic discovery of unit tests. -# Default CTest run includes per-case serial tests plus suite-level MPI variants -# for monoprop_MPI_TEST_PROCS. -discover_tests( +catch_discover_tests( monoprop_unit_tests.x PROPERTIES - LABELS - "unit" - SERIAL_ENVIRONMENT - ${_monoprop_serial_env_entries} + LABELS + unit +) + +configure_file( + ${CMAKE_CURRENT_LIST_DIR}/catch-properties.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/catch-properties.cmake + @ONLY ) +set_property( + DIRECTORY + APPEND + PROPERTY + TEST_INCLUDE_FILES + ${CMAKE_CURRENT_BINARY_DIR}/catch-properties.cmake +) + +set( + monoprop_MPI_TEST_PROCS + "2" + CACHE STRING + "Semicolon-separated list of ranks for MPI test variants" +) + +set(_monoprop_mpiexec "${MPIEXEC_EXECUTABLE}") +if(NOT _monoprop_mpiexec) + find_program( + _monoprop_mpiexec + NAMES + mpiexec + mpirun + ) +endif() + +set(_monoprop_mpiexec_numproc_flag "${MPIEXEC_NUMPROC_FLAG}") +if(NOT _monoprop_mpiexec_numproc_flag) + set(_monoprop_mpiexec_numproc_flag "-n") +endif() + +if(monoprop_ENABLE_MPI AND _monoprop_mpiexec) + set(_monoprop_mpi_ranks ${monoprop_MPI_TEST_PROCS}) + list(REMOVE_DUPLICATES _monoprop_mpi_ranks) + foreach(_monoprop_mpi_rank IN LISTS _monoprop_mpi_ranks) + if(NOT _monoprop_mpi_rank MATCHES "^[1-9][0-9]*$") + message( + FATAL_ERROR + "Invalid MPI rank '${_monoprop_mpi_rank}' in monoprop_MPI_TEST_PROCS='${monoprop_MPI_TEST_PROCS}'" + ) + endif() + + add_test( + NAME "monoprop_unit_tests.x_mpi_${_monoprop_mpi_rank}" + COMMAND + "${_monoprop_mpiexec}" "${_monoprop_mpiexec_numproc_flag}" + "${_monoprop_mpi_rank}" ${MPIEXEC_PREFLAGS} + $ --order lex --rng-seed 1 + ${MPIEXEC_POSTFLAGS} + ) + set_tests_properties( + "monoprop_unit_tests.x_mpi_${_monoprop_mpi_rank}" + PROPERTIES + WORKING_DIRECTORY + "${CMAKE_CURRENT_BINARY_DIR}" + LABELS + "unit;cxx;mpi;mpi-${_monoprop_mpi_rank}" + ENVIRONMENT + "OMPI_ALLOW_RUN_AS_ROOT=1;OMPI_ALLOW_RUN_AS_ROOT_CONFIRM=1" + ) + endforeach() +endif() diff --git a/cpp/tests/README.md b/cpp/tests/README.md index f167fe1f..b1492749 100644 --- a/cpp/tests/README.md +++ b/cpp/tests/README.md @@ -1,12 +1,12 @@ # C++ Test Suite -This directory contains the C++ test suite for monoprop, built using Boost.Test. +This directory contains the C++ test suite for monoprop, built using Catch2 v3. Every `*.cpp` here is globbed into a single executable, `monoprop_unit_tests.x`. ## Test Organization -Tests carry no labels of their own. The CTest harness (`boostAddTests.cmake`) -discovers every Boost.Test case and registers it twice: +Catch2's CMake integration discovers every `TEST_CASE` and registers it as a +`serial` CTest test. The project also registers suite-level MPI variants: - **`serial`**: the case run in-process with `MPI_COMM_SELF`. - **`mpi`** (+ rank-specific `mpi-`): the whole suite wrapped in @@ -30,6 +30,9 @@ uv sync --all-extras -v ctest --test-dir build/editable/Release ``` +Release builds compile these test sources at `-O1`; linked production objects +remain at `-O3`. + For an MPI-enabled tree, rerun `uv sync` with `--config-settings=cmake.define.monoprop_ENABLE_MPI=ON`. ## Running Tests @@ -44,15 +47,14 @@ ctest --test-dir build/editable/Release -L mpi-2 # only the 2-rank run Or drive the binary directly: ```bash -build/editable/Release/bin/monoprop_unit_tests.x --list_content -build/editable/Release/bin/monoprop_unit_tests.x --run_test=pauli_algebra_* +build/editable/Release/bin/monoprop_unit_tests.x --list-tests +build/editable/Release/bin/monoprop_unit_tests.x "pauli_algebra_*" mpirun -n 2 build/editable/Release/bin/monoprop_unit_tests.x ``` -Because CTest discovery treats each `--list_content` line as a top-level test -name and cannot address suite-nested cases, tests use flat -`BOOST_AUTO_TEST_CASE`s with a shared name prefix (e.g. `pauli_algebra_*`, -`inverted_index_*`) rather than `BOOST_AUTO_TEST_SUITE`. +Tests use stable, flat names with shared prefixes (for example, +`pauli_algebra_*` and `inverted_index_*`) so direct Catch2 test specs remain +predictable. ## Shared Test Utilities @@ -71,7 +73,8 @@ name and cannot address suite-nested cases, tests use flat (`core_with_gate`, `layer_with_gate`, `graph_with_gates`) for white-box MPGraph transform tests. - **`TestData.{h,cpp}`**: the `CaseData` struct and msgpack fixture loader. -- **`boost-test.cmake` / `boostAddTests.cmake`**: CMake test discovery. +- **`catch-properties.cmake.in`**: adds the project CTest labels and serial-only + MPI environment to Catch2's discovered tests. ## Test Files (by area) @@ -118,11 +121,11 @@ CMake wraps the whole suite in `mpiexec -n ` for each rank in per case, because the ranks have to reach the same collectives. For exhaustive rank coverage: `-Dmonoprop_MPI_TEST_PROCS='1;2;4'`. To run a single case under MPI while debugging, invoke the binary directly: -`mpirun -n 2 build/editable/Release/bin/monoprop_unit_tests.x --run_test=`. +`mpirun -n 2 build/editable/Release/bin/monoprop_unit_tests.x ""`. ## Adding New Tests -1. Add a `*.cpp` with flat `BOOST_AUTO_TEST_CASE`s (shared name prefix). +1. Add a `*.cpp` with Catch2 `TEST_CASE`s and a shared name prefix. 2. Reuse the shared helpers above rather than copying oracle/harness code. 3. For MPI-required scenarios, check `monoprop::mpi::size(MPI_COMM_WORLD)` and skip if `< 2`. diff --git a/cpp/tests/TestUtilities.h.in b/cpp/tests/TestUtilities.h.in index 37f590ed..6aa68058 100644 --- a/cpp/tests/TestUtilities.h.in +++ b/cpp/tests/TestUtilities.h.in @@ -18,50 +18,22 @@ #include #include #include -#include #include #include #include #include #include -#include #include #include #include -#include +#include +#include #include "TestData.h" #include "monoprop/MonomialPropagator.h" #include "monoprop/detail/mpi/MPICompat.h" -namespace detail { -template -concept Printable = requires(std::ostream& os, const T& value) { - { os << value } -> std::same_as; -}; -} // namespace detail - -// boost_test_print_type has to be in the same namespace as the printed type -namespace std { -template - requires detail::Printable -auto boost_test_print_type(std::ostream& os, const std::vector& aVec) -> std::ostream& { - os << "std::vector size " << aVec.size() << " ["; - for (const auto& i : aVec) { - os << "\n " << i; - } - os << "]"; - return os; -} -template - requires detail::Printable && detail::Printable -auto boost_test_print_type(std::ostream& os, const std::pair& aPair) -> std::ostream& { - os << "[" << aPair.first << ", " << aPair.second << "]"; - return os; -} -} // namespace std - namespace test_utils { namespace fs = std::filesystem; using namespace monoprop; @@ -73,7 +45,8 @@ static inline auto test_data_path() -> fs::path { template inline auto load_case_data(const std::string& filename) -> CaseData { const fs::path data_path = test_data_path() / filename; - BOOST_REQUIRE_MESSAGE(fs::exists(data_path), "Missing msgpack data file: " << data_path); + INFO("msgpack data file: " << data_path); + REQUIRE(fs::exists(data_path)); return load_case(data_path); } @@ -109,9 +82,9 @@ inline auto evaluate_expval(MonomialPropagator& sim, const CaseData& d } inline auto check_expval_close(const char* label, double expval, double exact, double atol = 1e-9) -> void { - BOOST_TEST_MESSAGE(std::string("[") + label + "] expval=" + std::format("{:.9f}", expval) - + ", exact=" + std::format("{:.9f}", exact)); - BOOST_CHECK_SMALL(expval - exact, atol); + INFO(std::string("[") + label + "] expval=" + std::format("{:.9f}", expval) + + ", exact=" + std::format("{:.9f}", exact)); + CHECK_THAT(expval - exact, Catch::Matchers::WithinAbs(0.0, atol)); } // Mixed absolute/relative comparison; rtol absorbs the accumulation drift between n=1 and n>1 runs. @@ -130,8 +103,9 @@ inline auto test_evolve_build_graph(const CaseData& data, const SimulatorConfig& const std::optional pare_threshold = pare ? std::optional{1e-10} : std::nullopt; auto expval_fn = mp.expectation_value_functional(pare_threshold); double expval = expval_fn(data.parameters); - BOOST_TEST_CONTEXT("n_modes=" << n_modes << " pare=" << pare << " sch_cutoff=" - << (cfg.schrodinger_cutoff ? std::to_string(*cfg.schrodinger_cutoff) : "none")) { + { + INFO("n_modes=" << n_modes << " pare=" << pare << " sch_cutoff=" + << (cfg.schrodinger_cutoff ? std::to_string(*cfg.schrodinger_cutoff) : "none")); check_expval_close("Expectation Value Build Graph", expval, exact_expval); } } @@ -149,8 +123,9 @@ inline auto test_evolve_build_graph_with_coeffs(const CaseData& data, const std::optional pare_threshold = pare ? std::optional{1e-10} : std::nullopt; auto expval_fn = mp.expectation_value_functional(pare_threshold); double expval = expval_fn(data.parameters); - BOOST_TEST_CONTEXT("n_modes=" << n_modes << " pare=" << pare << " sch_cutoff=" - << (cfg.schrodinger_cutoff ? std::to_string(*cfg.schrodinger_cutoff) : "none")) { + { + INFO("n_modes=" << n_modes << " pare=" << pare << " sch_cutoff=" + << (cfg.schrodinger_cutoff ? std::to_string(*cfg.schrodinger_cutoff) : "none")); check_expval_close("Expectation Value Build Graph with coeffs", expval, exact_expval); } } @@ -194,7 +169,8 @@ inline auto test_evolve_build_graph_with_coeffs_extend(const CaseData& data, const std::optional pare_threshold = pare ? std::optional{1e-10} : std::nullopt; auto expval_fn = mp.expectation_value_functional(pare_threshold); double expval = expval_fn(data.parameters); - BOOST_TEST_CONTEXT("n_modes=" << n_modes << " pare=" << pare) { + { + INFO("n_modes=" << n_modes << " pare=" << pare); check_expval_close("Expectation Value Build Graph with coeffs (split build, schrodinger)", expval, exact_expval); diff --git a/cpp/tests/ThreadHarness.h b/cpp/tests/ThreadHarness.h index 1d8af18f..9d0166d7 100644 --- a/cpp/tests/ThreadHarness.h +++ b/cpp/tests/ThreadHarness.h @@ -20,7 +20,7 @@ namespace test_utils { -// Body exceptions are captured per-rank and returned for the caller to check: Boost.Test assertions +// Body exceptions are captured per-rank and returned for the caller to check: Catch2 assertions // are only safe on the main thread. template auto run_comm_threads(Comm &comm, int s, Body body) -> std::vector { diff --git a/cpp/tests/bitset_tests.cpp b/cpp/tests/bitset_tests.cpp index 6a3bac26..2c64021e 100644 --- a/cpp/tests/bitset_tests.cpp +++ b/cpp/tests/bitset_tests.cpp @@ -15,7 +15,9 @@ // Bitset.h in isolation (single-word and multi-word) against a std::bitset oracle, so a regression // in the hand-rolled shift / scan / mask surfaces here rather than as a distant energy drift. -#include +#include +#include +#include #include #include @@ -42,55 +44,56 @@ auto make_pair(const std::vector &positions) -> std::pair, std template auto expect_equal(const Bitset &bs, const std::bitset &ref) -> void { for (size_t i = 0; i < N; ++i) { - BOOST_TEST(bs.test(i) == ref.test(i), "bit " << i); + INFO("bit " << i); + CHECK(bs.test(i) == ref.test(i)); } - BOOST_TEST(bs.count() == ref.count()); + CHECK(bs.count() == ref.count()); } } // namespace // The ctor masks off bits beyond NumBits (kTopMask), so a partial top word never leaks stray high bits. -BOOST_AUTO_TEST_CASE(bitset_ctor_sanitizes_top) { +TEST_CASE("bitset_ctor_sanitizes_top") { const Bitset<10> b(0xFFFFULL); - BOOST_TEST(b.count() == 10U); - BOOST_TEST(b.word(0) == 0x3FFULL); + CHECK(b.count() == 10U); + CHECK(b.word(0) == 0x3FFULL); const Bitset<64> full(~uint64_t{0}); - BOOST_TEST(full.count() == 64U); + CHECK(full.count() == 64U); } -BOOST_AUTO_TEST_CASE(bitset_set_test_word_boundaries) { +TEST_CASE("bitset_set_test_word_boundaries") { auto [bs, ref] = make_pair<100>({0, 63, 64, 99}); expect_equal<100>(bs, ref); - BOOST_TEST(bs.test(63)); - BOOST_TEST(bs.test(64)); - BOOST_TEST(!bs.test(62)); - BOOST_TEST(!bs.test(65)); - BOOST_TEST(bs.count() == 4U); + CHECK(bs.test(63)); + CHECK(bs.test(64)); + CHECK(!bs.test(62)); + CHECK(!bs.test(65)); + CHECK(bs.count() == 4U); } -BOOST_AUTO_TEST_CASE(bitset_count_and_parity_and_cross_word) { +TEST_CASE("bitset_count_and_parity_and_cross_word") { auto [a, ra] = make_pair<192>({1, 63, 64, 130, 191}); auto [b, rb] = make_pair<192>({63, 64, 65, 130}); const size_t expected = (ra & rb).count(); - BOOST_TEST(a.count_and(b) == expected); - BOOST_TEST(a.parity_and(b) == ((expected & 1U) != 0U)); + CHECK(a.count_and(b) == expected); + CHECK(a.parity_and(b) == ((expected & 1U) != 0U)); auto [c, rc] = make_pair<192>({0, 2, 4}); auto [d, rd] = make_pair<192>({1, 3, 5}); - BOOST_TEST(c.count_and(d) == 0U); - BOOST_TEST(!c.parity_and(d)); + CHECK(c.count_and(d) == 0U); + CHECK(!c.parity_and(d)); } -BOOST_AUTO_TEST_CASE(bitset_not_respects_top_mask) { - BOOST_TEST((~Bitset<100>{}).count() == 100U); - BOOST_TEST((~Bitset<64>{}).count() == 64U); - BOOST_TEST((~Bitset<10>{}).count() == 10U); +TEST_CASE("bitset_not_respects_top_mask") { + CHECK((~Bitset<100>{}).count() == 100U); + CHECK((~Bitset<64>{}).count() == 64U); + CHECK((~Bitset<10>{}).count() == 10U); auto [bs, ref] = make_pair<100>({3, 70, 99}); - BOOST_TEST((~~bs) == bs); + CHECK((~~bs) == bs); (void)ref; } // Shift amounts cover exact word multiples, sub-word crossings, and >= NumBits (which must zero the set). -BOOST_AUTO_TEST_CASE(bitset_shift_right_cross_word) { +TEST_CASE("bitset_shift_right_cross_word") { const std::vector pos{0, 5, 63, 64, 65, 130, 191}; for (size_t s : {size_t{0}, size_t{1}, @@ -113,39 +116,39 @@ BOOST_AUTO_TEST_CASE(bitset_shift_right_cross_word) { expect_equal<64>(bs, ref >> 8); } -BOOST_AUTO_TEST_CASE(bitset_find_first_next_chain) { +TEST_CASE("bitset_find_first_next_chain") { auto [bs, ref] = make_pair<192>({5, 63, 64, 130, 191}); (void)ref; - BOOST_TEST(bs.find_first() == 5U); - BOOST_TEST(bs.find_next(5) == 63U); - BOOST_TEST(bs.find_next(63) == 64U); - BOOST_TEST(bs.find_next(64) == 130U); - BOOST_TEST(bs.find_next(130) == 191U); - BOOST_TEST(bs.find_next(191) == 192U); // past the last set bit -> NumBits - BOOST_TEST(Bitset<192>{}.find_first() == 192U); + CHECK(bs.find_first() == 5U); + CHECK(bs.find_next(5) == 63U); + CHECK(bs.find_next(63) == 64U); + CHECK(bs.find_next(64) == 130U); + CHECK(bs.find_next(130) == 191U); + CHECK(bs.find_next(191) == 192U); // past the last set bit -> NumBits + CHECK(Bitset<192>{}.find_first() == 192U); // Single-word find_next branch. auto [sb, sref] = make_pair<64>({0, 40}); (void)sref; - BOOST_TEST(sb.find_first() == 0U); - BOOST_TEST(sb.find_next(0) == 40U); - BOOST_TEST(sb.find_next(40) == 64U); + CHECK(sb.find_first() == 0U); + CHECK(sb.find_next(0) == 40U); + CHECK(sb.find_next(40) == 64U); } // The multi-word hash must depend on which word carries a bit (the +i mix guard), and be deterministic. -BOOST_AUTO_TEST_CASE(bitset_splitmix_hash_position_sensitive) { +TEST_CASE("bitset_splitmix_hash_position_sensitive") { Bitset<128> low; low.set(0); Bitset<128> high; high.set(64); // bit 0 of word 1 — same intra-word position as `low`'s bit const std::hash> h; - BOOST_TEST(h(low) != h(high)); - BOOST_TEST(h(low) == h(low)); + CHECK(h(low) != h(high)); + CHECK(h(low) == h(low)); Bitset<128> low_copy; low_copy.set(0); - BOOST_TEST(h(low) == h(low_copy)); + CHECK(h(low) == h(low_copy)); } -BOOST_AUTO_TEST_CASE(bitset_random_differential) { +TEST_CASE("bitset_random_differential") { constexpr size_t N = 128; std::mt19937_64 rng(0xB175E7ULL); std::uniform_int_distribution bit(0, N - 1); @@ -163,7 +166,7 @@ BOOST_AUTO_TEST_CASE(bitset_random_differential) { expect_equal(a ^ b, ra ^ rb); const size_t s = bit(rng); expect_equal(a >> s, ra >> s); - BOOST_TEST(a.count_and(b) == (ra & rb).count()); - BOOST_TEST((a == b) == (ra == rb)); + CHECK(a.count_and(b) == (ra & rb).count()); + CHECK((a == b) == (ra == rb)); } } diff --git a/cpp/tests/boost-test.cmake b/cpp/tests/boost-test.cmake deleted file mode 100644 index 7d0d52c9..00000000 --- a/cpp/tests/boost-test.cmake +++ /dev/null @@ -1,108 +0,0 @@ -set( - monoprop_MPI_TEST_PROCS - "2" - CACHE STRING - "Semicolon-separated list of ranks for MPI test variants" -) - -set(_monoprop_mpiexec "${MPIEXEC_EXECUTABLE}") -if(NOT _monoprop_mpiexec) - find_program( - _monoprop_mpiexec_fallback - NAMES - mpiexec - mpirun - ) - set(_monoprop_mpiexec "${_monoprop_mpiexec_fallback}") -endif() - -set(_monoprop_mpiexec_numproc_flag "${MPIEXEC_NUMPROC_FLAG}") -if(NOT _monoprop_mpiexec_numproc_flag) - set(_monoprop_mpiexec_numproc_flag "-n") -endif() - -# SERIAL_ENVIRONMENT: VAR=value entries applied to the per-case `serial` variants only. -function(discover_tests TARGET) - cmake_parse_arguments( - "" - "" - "WORKING_DIRECTORY" - "EXTRA_ARGS;PROPERTIES;SERIAL_ENVIRONMENT" - ${ARGN} - ) - - if(NOT _WORKING_DIRECTORY) - set(_WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") - endif() - if(NOT _TEST_LIST) - set(_TEST_LIST ${TARGET}_TESTS) - endif() - - ## Generate a unique name based on the extra arguments - string(SHA1 args_hash "${_TEST_SPEC} ${_EXTRA_ARGS}") - string(SUBSTRING ${args_hash} 0 7 args_hash) - - # Define rule to generate test list for aforementioned test executable - set( - ctest_include_file - "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}_include-${args_hash}.cmake" - ) - set( - ctest_tests_file - "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}_tests-${args_hash}.cmake" - ) - if(_monoprop_mpiexec AND monoprop_ENABLE_MPI) - set(_enable_mpi_variants "ON") - else() - set(_enable_mpi_variants "OFF") - endif() - - add_custom_command( - TARGET ${TARGET} - POST_BUILD - BYPRODUCTS - "${ctest_tests_file}" - COMMAND - "${CMAKE_COMMAND}" -D "TEST_TARGET=${TARGET}" -D - "TEST_EXECUTABLE=$" -D - "TEST_WORKING_DIR=${_WORKING_DIRECTORY}" -D - "TEST_EXTRA_ARGS=${_EXTRA_ARGS}" -D "TEST_PROPERTIES=${_PROPERTIES}" -D - "TEST_SERIAL_ENVIRONMENT=${_SERIAL_ENVIRONMENT}" -D - "TEST_LIST=${_TEST_LIST}" -D "CTEST_FILE=${ctest_tests_file}" -D - "TEST_ENABLE_MPI_VARIANTS=${_enable_mpi_variants}" -D - "TEST_MPI_NUMPROCS=${monoprop_MPI_TEST_PROCS}" -D - "MPIEXEC_EXECUTABLE=${_monoprop_mpiexec}" -D - "MPIEXEC_NUMPROC_FLAG=${_monoprop_mpiexec_numproc_flag}" -D - "MPIEXEC_PREFLAGS=${MPIEXEC_PREFLAGS}" -D - "MPIEXEC_POSTFLAGS=${MPIEXEC_POSTFLAGS}" -P "${_DISCOVER_TESTS_SCRIPT}" - VERBATIM - ) - - file( - WRITE "${ctest_include_file}" - "if(EXISTS \"${ctest_tests_file}\")\n" - " include(\"${ctest_tests_file}\")\n" - "else()\n" - " add_test(${TARGET}_NOT_BUILT-${args_hash} ${TARGET}_NOT_BUILT-${args_hash})\n" - "endif()\n" - ) - - # Add discovered tests to directory TEST_INCLUDE_FILES - set_property( - DIRECTORY - APPEND - PROPERTY - TEST_INCLUDE_FILES - "${ctest_include_file}" - ) -endfunction() - -############################################################################### - -set( - _DISCOVER_TESTS_SCRIPT - ${CMAKE_CURRENT_LIST_DIR}/boostAddTests.cmake - CACHE INTERNAL - "The location of the boostAddTests script" -) -mark_as_advanced(_DISCOVER_TESTS_SCRIPT) diff --git a/cpp/tests/boostAddTests.cmake b/cpp/tests/boostAddTests.cmake deleted file mode 100644 index 74b5ff00..00000000 --- a/cpp/tests/boostAddTests.cmake +++ /dev/null @@ -1,257 +0,0 @@ -if(NOT DEFINED TEST_ENABLE_MPI_VARIANTS) - set(TEST_ENABLE_MPI_VARIANTS "OFF") -endif() -if(NOT DEFINED TEST_MPI_NUMPROCS) - set(TEST_MPI_NUMPROCS "2") -endif() -if(TEST_ENABLE_MPI_VARIANTS AND NOT MPIEXEC_EXECUTABLE) - message( - WARNING - "Requested MPI variants for tests but MPIEXEC_EXECUTABLE is not set" - ) - set(TEST_ENABLE_MPI_VARIANTS "OFF") -endif() -if(TEST_ENABLE_MPI_VARIANTS AND NOT MPIEXEC_NUMPROC_FLAG) - message( - FATAL_ERROR - "MPI variants require MPIEXEC_NUMPROC_FLAG to select rank counts" - ) -endif() - -set(_mpi_ranks) -if(TEST_ENABLE_MPI_VARIANTS) - if("${TEST_MPI_NUMPROCS}" STREQUAL "") - set(TEST_MPI_NUMPROCS 2) - endif() - - foreach(_rank IN LISTS TEST_MPI_NUMPROCS) - if(NOT _rank MATCHES "^[1-9][0-9]*$") - message( - FATAL_ERROR - "Invalid MPI rank '${_rank}' in TEST_MPI_NUMPROCS='${TEST_MPI_NUMPROCS}'. Use positive integers." - ) - endif() - endforeach() - - set(_mpi_ranks ${TEST_MPI_NUMPROCS}) - list(REMOVE_DUPLICATES _mpi_ranks) -endif() - -set(extra_args ${TEST_EXTRA_ARGS}) -set(properties ${TEST_PROPERTIES}) -set(serial_env ${TEST_SERIAL_ENVIRONMENT}) -set(script) -set(tests) - -# LABELS and ENVIRONMENT are split off because each variant extends them; every other caller -# property is inherited verbatim. -set(common_properties) -set(common_labels_list) -set(common_env_list) -list(LENGTH properties _common_prop_len) -set(_common_prop_idx 0) -while(_common_prop_idx LESS _common_prop_len) - math(EXPR _common_prop_next "${_common_prop_idx} + 1") - list(GET properties ${_common_prop_idx} _common_prop_key) - set(_common_prop_value "") - if(_common_prop_next LESS _common_prop_len) - list(GET properties ${_common_prop_next} _common_prop_value) - endif() - if(_common_prop_key STREQUAL "LABELS") - if(NOT _common_prop_value STREQUAL "") - list(APPEND common_labels_list ${_common_prop_value}) - endif() - elseif(_common_prop_key STREQUAL "ENVIRONMENT") - if(NOT _common_prop_value STREQUAL "") - list(APPEND common_env_list ${_common_prop_value}) - endif() - else() - list( - APPEND common_properties - "${_common_prop_key}" - "${_common_prop_value}" - ) - endif() - math(EXPR _common_prop_idx "${_common_prop_idx} + 2") -endwhile() - -list(APPEND common_labels_list cxx) -list(REMOVE_DUPLICATES common_labels_list) -list(REMOVE_DUPLICATES common_env_list) - -function(add_command NAME) - set(_args "") - foreach(_arg ${ARGN}) - if(_arg MATCHES "^\\[=+\\[.*\\]=+\\]$") - set(_args "${_args} ${_arg}") - elseif(_arg MATCHES "[^-./:a-zA-Z0-9_]") - set(_args "${_args} [==[${_arg}]==]") # form a bracket_argument - else() - set(_args "${_args} ${_arg}") - endif() - endforeach() - set(script "${script}${NAME}(${_args})\n" PARENT_SCOPE) -endfunction() - -# `script` and `tests` are written back because add_command's PARENT_SCOPE write lands here. -function(register_variant NAME) - cmake_parse_arguments("" "" "" "COMMAND;LABELS;ENVIRONMENT" ${ARGN}) - - set( - _variant_labels - ${common_labels_list} - ${_LABELS} - ) - list(REMOVE_DUPLICATES _variant_labels) - set( - _variant_env - ${common_env_list} - ${_ENVIRONMENT} - ) - list(REMOVE_DUPLICATES _variant_env) - - set(_variant_properties ${common_properties}) - # Bracket-quote the joined values: they hold the `;` separators CTest expects, which would - # otherwise be re-split when the generated script is included. - if(_variant_labels) - list(JOIN _variant_labels ";" _labels_value) - list( - APPEND _variant_properties - "LABELS" - "[==[${_labels_value}]==]" - ) - endif() - if(_variant_env) - list(JOIN _variant_env ";" _env_value) - list( - APPEND _variant_properties - "ENVIRONMENT" - "[==[${_env_value}]==]" - ) - endif() - - add_command(add_test "${NAME}" ${_COMMAND}) - add_command(set_tests_properties - "${NAME}" - PROPERTIES - WORKING_DIRECTORY "${TEST_WORKING_DIR}" - ${_variant_properties} - ) - - set(script "${script}" PARENT_SCOPE) - set( - tests - ${tests} - "${NAME}" - PARENT_SCOPE - ) -endfunction() - -# Run test executable to get list of available tests. Boost.Test has no CMake-side discovery -# module (no analogue of gtest_discover_tests), so parsing --list_content is the only option. -if(NOT EXISTS "${TEST_EXECUTABLE}") - message( - FATAL_ERROR - "Specified test executable '${TEST_EXECUTABLE}' does not exist" - ) -endif() - -execute_process( - COMMAND - "${TEST_EXECUTABLE}" --list_content=HRF --report_sink=stdout - OUTPUT_VARIABLE output - ERROR_VARIABLE err # it prints to stderr... - RESULT_VARIABLE result - WORKING_DIRECTORY "${TEST_WORKING_DIR}" -) -if(NOT ${result} EQUAL 0) - message( - FATAL_ERROR - "Error running test executable '${TEST_EXECUTABLE}':\n" - " Result: ${result}\n" - " Output: ${output}\n" - " Error: ${err}\n" - ) -endif() - -# Convert the raw output to a list of lines -string( - REPLACE "\n" - ";" - LINES - "${output}" -) - -# process each line -foreach(LINE ${LINES}) - # Remove trailing asterisk - string( - REGEX REPLACE "\\*$" - "" - CLEANED_LINE - "${LINE}" - ) - - # Trim whitespace - string(STRIP "${CLEANED_LINE}" test) - - # Check if the line doesn't contain _0, _1, etc. and is not empty - if(NOT test MATCHES "_[0-9]+" AND NOT "${test}" STREQUAL "") - register_variant("${test}" - COMMAND - "${TEST_EXECUTABLE}" - "--run_test=${test}" - "--report_level=detailed" - "--catch_system_errors=yes" - ${extra_args} - LABELS - serial - ENVIRONMENT - ${serial_env} - ) - endif() -endforeach() - -# The MPI variants wrap the WHOLE suite in one mpiexec per rank count: the ranks must reach the -# same collectives, which per-case launches cannot guarantee. -if(TEST_ENABLE_MPI_VARIANTS AND MPIEXEC_EXECUTABLE) - foreach(_mpi_rank IN LISTS _mpi_ranks) - set(mpi_cmd "${MPIEXEC_EXECUTABLE}") - list( - APPEND mpi_cmd - "${MPIEXEC_NUMPROC_FLAG}" - "${_mpi_rank}" - ) - if(MPIEXEC_PREFLAGS) - list(APPEND mpi_cmd ${MPIEXEC_PREFLAGS}) - endif() - list( - APPEND mpi_cmd - "${TEST_EXECUTABLE}" - "--report_level=detailed" - "--catch_system_errors=yes" - ${extra_args} - ) - if(MPIEXEC_POSTFLAGS) - list(APPEND mpi_cmd ${MPIEXEC_POSTFLAGS}) - endif() - - register_variant("${TEST_TARGET}_mpi_${_mpi_rank}" - COMMAND - ${mpi_cmd} - LABELS - mpi - "mpi-${_mpi_rank}" - ENVIRONMENT - "OMPI_ALLOW_RUN_AS_ROOT=1" - "OMPI_ALLOW_RUN_AS_ROOT_CONFIRM=1" - ) - endforeach() -endif() - -# Create a list of all discovered tests, which users may use to e.g. set -# properties on the tests -add_command(set ${TEST_LIST} ${tests}) - -# Write CTest script -file(WRITE "${CTEST_FILE}" "${script}") diff --git a/cpp/tests/build_graph_tests.cpp b/cpp/tests/build_graph_tests.cpp index 83e36d2b..a9bc1440 100644 --- a/cpp/tests/build_graph_tests.cpp +++ b/cpp/tests/build_graph_tests.cpp @@ -12,21 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include -#include -#include +#include +#include +#include +#include #include "TestUtilities.h" using namespace test_utils; -namespace utf = boost::unit_test; -namespace bdata = utf::data; -BOOST_DATA_TEST_CASE_F(ExampleDataFix, - build_graph_cases, - bdata::make(ds_pare_values) ^ bdata::make(ds_schrodinger_enabled), - pare, - sch_enabled) { +TEST_CASE_METHOD(ExampleDataFix, "build_graph_cases") { + const auto index = GENERATE(0U, 1U); + const auto pare = ds_pare_values[index]; + const auto sch_enabled = ds_schrodinger_enabled[index]; + CAPTURE(pare, sch_enabled); const auto schrodinger_cutoff = make_schrodinger_cutoff(sch_enabled, cutoff); SimulatorConfig cfg{ .schrodinger_cutoff = schrodinger_cutoff ? std::optional(*schrodinger_cutoff) : std::nullopt, @@ -36,11 +35,11 @@ BOOST_DATA_TEST_CASE_F(ExampleDataFix, test_evolve_build_graph(data, cfg, pare, data.actual_expval); } -BOOST_DATA_TEST_CASE_F(ExampleDataFix, - build_graph_with_coeffs_cases, - bdata::make(ds_pare_values) ^ bdata::make(ds_schrodinger_enabled), - pare, - sch_enabled) { +TEST_CASE_METHOD(ExampleDataFix, "build_graph_with_coeffs_cases") { + const auto index = GENERATE(0U, 1U); + const auto pare = ds_pare_values[index]; + const auto sch_enabled = ds_schrodinger_enabled[index]; + CAPTURE(pare, sch_enabled); const auto schrodinger_cutoff = make_schrodinger_cutoff(sch_enabled, cutoff); SimulatorConfig cfg{ .schrodinger_cutoff = schrodinger_cutoff ? std::optional(*schrodinger_cutoff) : std::nullopt, @@ -51,7 +50,9 @@ BOOST_DATA_TEST_CASE_F(ExampleDataFix, } // Schrodinger-only by construction; the reason is on test_evolve_build_graph_with_coeffs_extend. -BOOST_DATA_TEST_CASE_F(ExampleDataFix, build_graph_with_coeffs_extend_cases, bdata::make(ds_pare_values), pare) { +TEST_CASE_METHOD(ExampleDataFix, "build_graph_with_coeffs_extend_cases") { + const auto pare = GENERATE(false, true); + CAPTURE(pare); const auto schrodinger_cutoff = make_schrodinger_cutoff(/*enabled=*/true, cutoff); SimulatorConfig cfg{ .schrodinger_cutoff = std::optional(*schrodinger_cutoff), @@ -62,7 +63,7 @@ BOOST_DATA_TEST_CASE_F(ExampleDataFix, build_graph_with_coeffs_extend_cases, bda } // graph_size().first counts cos-scaled non-endpoints, recomputed from the operator's inverted index. -BOOST_AUTO_TEST_CASE(graph_size_reports_real_cosine_only_count) { +TEST_CASE("graph_size_reports_real_cosine_only_count") { constexpr size_t N = 8; const auto data = test_utils::load_case_data("random_exact.msgpack"); @@ -74,11 +75,11 @@ BOOST_AUTO_TEST_CASE(graph_size_reports_real_cosine_only_count) { // Truncating cutoff: some cos-scaled terms lose their sine partner, so cosine-only is positive. const auto truncated = sized(8); - BOOST_CHECK_GT(truncated.first, 0U); - BOOST_CHECK_GT(truncated.second, 0U); + CHECK((truncated.first) > (0U)); + CHECK((truncated.second) > (0U)); // Exact cutoff: every cos index is also a rotation endpoint, so cosine-only is genuinely zero. const auto exact = sized(2 * N); - BOOST_CHECK_EQUAL(exact.first, 0U); - BOOST_CHECK_GT(exact.second, truncated.second); + CHECK((exact.first) == (0U)); + CHECK((exact.second) > (truncated.second)); } diff --git a/cpp/tests/catch-properties.cmake.in b/cpp/tests/catch-properties.cmake.in new file mode 100644 index 00000000..d41acec9 --- /dev/null +++ b/cpp/tests/catch-properties.cmake.in @@ -0,0 +1,26 @@ +foreach(_monoprop_test_metadata IN LISTS monoprop_unit_tests.x_TESTS) + string( + REGEX MATCH "\"name\"[ \t\r\n]*:[ \t\r\n]*\"([^\"]+)\"" + _monoprop_test_name_match + "${_monoprop_test_metadata}" + ) + if(_monoprop_test_name_match) + set(_monoprop_test "${CMAKE_MATCH_1}") + else() + set(_monoprop_test "${_monoprop_test_metadata}") + endif() + set_tests_properties( + "${_monoprop_test}" + PROPERTIES + LABELS + "unit;cxx;serial" + ) + if(NOT "@_monoprop_serial_env_entries@" STREQUAL "") + set_tests_properties( + "${_monoprop_test}" + PROPERTIES + ENVIRONMENT + "@_monoprop_serial_env_entries@" + ) + endif() +endforeach() diff --git a/cpp/tests/combined_recompute_equivalence.cpp b/cpp/tests/combined_recompute_equivalence.cpp index 58f9db27..862c0f11 100644 --- a/cpp/tests/combined_recompute_equivalence.cpp +++ b/cpp/tests/combined_recompute_equivalence.cpp @@ -16,7 +16,9 @@ // bit-for-bit with the materialised-fold oracle (make_fold_cache + the scale_cos_cached / // accumulate_cos_cached replays below), on every layer of a real propagated operator. -#include +#include +#include +#include #include #include @@ -77,7 +79,7 @@ double accumulate_cos_cached(const monoprop::detail::FoldCache &p, // scale: coeff[i] *= cos over the layer's cosine index set — a pure per-index scatter, so the two // paths must produce byte-identical arrays. -BOOST_AUTO_TEST_CASE(combined_scale_cache_equals_recompute) { +TEST_CASE("combined_scale_cache_equals_recompute") { const auto data = load_case_data("random_exact.msgpack"); SimulatorConfig cfg{.comm = MPI_COMM_SELF}; auto sim = build_simulator(data, cfg); @@ -86,7 +88,7 @@ BOOST_AUTO_TEST_CASE(combined_scale_cache_equals_recompute) { const auto &inverted_index = sim.mp_op().inverted_index(); const auto &graph = sim.graph(); const size_t n = sim.mp_op().size(); - BOOST_REQUIRE(n > 0); + REQUIRE(n > 0); // Distinct, non-degenerate coefficients so a missed/extra index shows up. std::vector baseline(n); @@ -114,16 +116,16 @@ BOOST_AUTO_TEST_CASE(combined_scale_cache_equals_recompute) { scale_cos_cached(prepared, a.data(), cos_val); monoprop::detail::scale_cos_lazy(inverted_index, recipe, b.data(), cos_val); - BOOST_TEST_INFO("layer " << li); - BOOST_TEST(std::memcmp(a.data(), b.data(), n * sizeof(double)) == 0); + INFO("layer " << li); + CHECK(std::memcmp(a.data(), b.data(), n * sizeof(double)) == 0); } // The fixture must actually exercise the odd-|G| parity correction, or the guardrail is hollow. - BOOST_TEST(odd_layers > 0u); + CHECK(odd_layers > 0u); } // accumulate: the per-index state/ham mutations must be byte-identical; the returned reduction may be // summed in a different order, so it is compared within a tight fp tolerance. -BOOST_AUTO_TEST_CASE(combined_accumulate_cache_equals_recompute) { +TEST_CASE("combined_accumulate_cache_equals_recompute") { const auto data = load_case_data("random_exact.msgpack"); SimulatorConfig cfg{.comm = MPI_COMM_SELF}; auto sim = build_simulator(data, cfg); @@ -132,7 +134,7 @@ BOOST_AUTO_TEST_CASE(combined_accumulate_cache_equals_recompute) { const auto &inverted_index = sim.mp_op().inverted_index(); const auto &graph = sim.graph(); const size_t n = sim.mp_op().size(); - BOOST_REQUIRE(n > 0); + REQUIRE(n > 0); std::vector state0(n); std::vector ham0(n); @@ -162,16 +164,16 @@ BOOST_AUTO_TEST_CASE(combined_accumulate_cache_equals_recompute) { cos_val, sec_val); - BOOST_TEST_INFO("layer " << li); - BOOST_TEST(std::memcmp(sa.data(), sb.data(), n * sizeof(double)) == 0); - BOOST_TEST_INFO("layer " << li); - BOOST_TEST(std::memcmp(ha.data(), hb.data(), n * sizeof(double)) == 0); - BOOST_CHECK_SMALL(std::abs(ea - eb), 1e-9 * (1.0 + std::abs(ea))); + INFO("layer " << li); + CHECK(std::memcmp(sa.data(), sb.data(), n * sizeof(double)) == 0); + INFO("layer " << li); + CHECK(std::memcmp(ha.data(), hb.data(), n * sizeof(double)) == 0); + CHECK_THAT(std::abs(ea - eb), Catch::Matchers::WithinAbs(0.0, 1e-9 * (1.0 + std::abs(ea)))); } } // Lives here because it re-runs the same recompute machinery exercised above. -BOOST_FIXTURE_TEST_CASE(snapshot_invariance_repeated_evaluation, ExampleDataFix) { +TEST_CASE_METHOD(ExampleDataFix, "snapshot_invariance_repeated_evaluation") { SimulatorConfig cfg{.comm = MPI_COMM_SELF}; auto sim = build_simulator(data, cfg); sim.build_graph(data.majoranas, data.param_inds, data.gen_coeffs); @@ -180,15 +182,15 @@ BOOST_FIXTURE_TEST_CASE(snapshot_invariance_repeated_evaluation, ExampleDataFix) const double e1 = fn(data.parameters); const double e2 = fn(data.parameters); - BOOST_CHECK_SMALL(e1 - e2, 1e-13); - BOOST_TEST_MESSAGE("snapshot_invariance energy=" << e1); + CHECK_THAT(e1 - e2, Catch::Matchers::WithinAbs(0.0, 1e-13)); + INFO("snapshot_invariance energy=" << e1); } // Lifetime contract: a LazyFold outlives the index it was built from (build_cos_callbacks retains one // per layer in a functional's closure, and a later build_graph rebuilds InvertedIndex::row_parity_), // so it must hold no pointer into that buffer. Pins both halves — that the buffer really does move // under growth, and that a fold built before the growth still folds like a FoldCache built after it. -BOOST_AUTO_TEST_CASE(lazy_fold_survives_operator_growth) { +TEST_CASE("lazy_fold_survives_operator_growth") { const auto data = load_case_data("random_exact.msgpack"); SimulatorConfig cfg{.comm = MPI_COMM_SELF}; auto sim = build_simulator(data, cfg); @@ -204,21 +206,21 @@ BOOST_AUTO_TEST_CASE(lazy_fold_survives_operator_growth) { break; } } - BOOST_REQUIRE(odd_layer < graph.layers()); + REQUIRE(odd_layer < graph.layers()); const auto layer = graph.get_layer_traversal(odd_layer); const auto gen = generator_of(layer); const auto scaled_count = layer.scaled_count(); const uint64_t *before = sim.mp_op().inverted_index().row_parity_words(); - BOOST_REQUIRE(before != nullptr); + REQUIRE(before != nullptr); auto recipe = monoprop::detail::make_lazy_fold(sim.mp_op().inverted_index(), gen, scaled_count, kBasis); // Grow the operator, forcing the index and its row parity onto fresh storage. sim.build_graph(data.majoranas, data.param_inds, data.gen_coeffs); const uint64_t *after = sim.mp_op().inverted_index().row_parity_words(); - BOOST_REQUIRE(after != nullptr); - BOOST_TEST(before != after); // a pointer cached in the fold would now dangle + REQUIRE(after != nullptr); + CHECK(before != after); // a pointer cached in the fold would now dangle const size_t n = sim.mp_op().size(); std::vector baseline(n); @@ -234,5 +236,5 @@ BOOST_AUTO_TEST_CASE(lazy_fold_survives_operator_growth) { scale_cos_cached(prepared, expected.data(), cos_val); monoprop::detail::scale_cos_lazy(sim.mp_op().inverted_index(), recipe, actual.data(), cos_val); - BOOST_TEST(std::memcmp(expected.data(), actual.data(), n * sizeof(double)) == 0); + CHECK(std::memcmp(expected.data(), actual.data(), n * sizeof(double)) == 0); } diff --git a/cpp/tests/cpu_topology_tests.cpp b/cpp/tests/cpu_topology_tests.cpp index 1f64526e..a2359907 100644 --- a/cpp/tests/cpu_topology_tests.cpp +++ b/cpp/tests/cpu_topology_tests.cpp @@ -21,7 +21,9 @@ // so the L3-domain interleaving and MPI-rank slicing logic can be checked deterministically // without depending on live hardware or hwloc. -#include +#include +#include +#include #include #include @@ -56,7 +58,7 @@ auto empty_placement_is_licensed() -> bool { if (monoprop::config::get().partition_pinning) { return false; } - BOOST_TEST_MESSAGE("monoprop_PARTITION_PINNING is off; partition_cpusets places nothing"); + INFO("monoprop_PARTITION_PINNING is off; partition_cpusets places nothing"); return true; } @@ -64,29 +66,29 @@ auto empty_placement_is_licensed() -> bool { /* ── Live smoke tests ─────────────────────────────────────────────────────── */ -BOOST_AUTO_TEST_CASE(cpu_topology_enumerate_and_place) { +TEST_CASE("cpu_topology_enumerate_and_place") { const auto cores = partition::enumerate_physical_cores(); AffinityGuard guard; // save affinity before any potential pin const auto one = partition::partition_cpusets(/*n=*/1); - BOOST_CHECK(one.size() <= 1u); + CHECK(one.size() <= 1u); if (!one.empty()) { // A placement only comes back when topology discovery succeeded and pinning is enabled. - BOOST_CHECK(!cores.empty()); + CHECK(!cores.empty()); partition::pin_this_thread(one.front()); // guard restores affinity on scope exit } // When topology discovery succeeds, a non-empty core list must produce a non-empty placement. if (!cores.empty() && !(one.empty() && empty_placement_is_licensed())) { - BOOST_CHECK_EQUAL(one.size(), 1u); + CHECK((one.size()) == (1u)); } // Oversubscription must always return empty regardless of topology state. const auto too_many = partition::partition_cpusets(/*n=*/1'000'000); - BOOST_CHECK(too_many.empty()); + CHECK(too_many.empty()); } -BOOST_AUTO_TEST_CASE(cpu_topology_place_co_located_ranks) { +TEST_CASE("cpu_topology_place_co_located_ranks") { const auto cores = partition::enumerate_physical_cores(); if (cores.size() < 2) { return; // need at least two cores for the disjoint-placement check @@ -98,22 +100,22 @@ BOOST_AUTO_TEST_CASE(cpu_topology_place_co_located_ranks) { const auto rank0 = partition::partition_cpusets(/*n=*/1, /*group_index=*/0, /*group_count=*/2); const auto rank1 = partition::partition_cpusets(/*n=*/1, /*group_index=*/1, /*group_count=*/2); if (rank0.empty() || rank1.empty()) { - BOOST_CHECK(rank0.empty()); - BOOST_CHECK(rank1.empty()); + CHECK(rank0.empty()); + CHECK(rank1.empty()); return; } - BOOST_REQUIRE_EQUAL(rank0.size(), 1u); - BOOST_REQUIRE_EQUAL(rank1.size(), 1u); + REQUIRE((rank0.size()) == (1u)); + REQUIRE((rank1.size()) == (1u)); // The two placements must be on distinct PUs; sharing would violate the MPI no-starvation // invariant (one rank's busy-polling collectives cannot starve the other's barrier spins). - BOOST_CHECK(rank0.front().pu != rank1.front().pu); + CHECK(rank0.front().pu != rank1.front().pu); // Both arms passed explicitly so neither depends on the host: refuse the shared one, fill the private. const auto shared_mask = partition::partition_cpusets(/*n=*/cores.size(), /*group_index=*/1, /*group_count=*/2, partition::NodeMask::Shared); - BOOST_CHECK(shared_mask.empty()); + CHECK(shared_mask.empty()); const auto private_mask = partition::partition_cpusets(/*n=*/cores.size(), /*group_index=*/1, @@ -122,126 +124,126 @@ BOOST_AUTO_TEST_CASE(cpu_topology_place_co_located_ranks) { if (private_mask.empty() && empty_placement_is_licensed()) { return; } - BOOST_REQUIRE_EQUAL(private_mask.size(), cores.size()); + REQUIRE((private_mask.size()) == (cores.size())); std::set placed; for (const auto &set : private_mask) { placed.insert(set.pu); } - BOOST_CHECK_EQUAL(placed.size(), cores.size()); + CHECK((placed.size()) == (cores.size())); std::set visible; for (const auto &core : cores) { visible.insert(core.cpu); } for (const int pu : placed) { - BOOST_CHECK(visible.count(pu) == 1); + CHECK(visible.count(pu) == 1); } } /* ── Policy unit tests (deterministic, no hwloc or live hardware) ─────────── */ -BOOST_AUTO_TEST_CASE(cpu_topology_policy_interleave_across_l3) { +TEST_CASE("cpu_topology_policy_interleave_across_l3") { // 4 cores across 2 L3 domains; single rank receives all. // by_domain[0] = {0, 4}, by_domain[1] = {2, 6} // depth-first interleave: 0, 2, 4, 6 const std::vector cores = {{0, 0}, {2, 1}, {4, 0}, {6, 1}}; const auto order = placement_order(cores, 4, 0, 1); - BOOST_REQUIRE_EQUAL(order.size(), 4u); - BOOST_CHECK_EQUAL(order[0], 0); - BOOST_CHECK_EQUAL(order[1], 2); - BOOST_CHECK_EQUAL(order[2], 4); - BOOST_CHECK_EQUAL(order[3], 6); + REQUIRE((order.size()) == (4u)); + CHECK((order[0]) == (0)); + CHECK((order[1]) == (2)); + CHECK((order[2]) == (4)); + CHECK((order[3]) == (6)); } -BOOST_AUTO_TEST_CASE(cpu_topology_policy_disjoint_mpi_ranks) { +TEST_CASE("cpu_topology_policy_disjoint_mpi_ranks") { // 4 cores across 2 L3 domains; 2 co-located ranks each get 1 partition. // rank0 is dealt domain 0, rank1 is dealt domain 1 ⇒ no shared PU. const std::vector cores = {{0, 0}, {2, 1}, {4, 0}, {6, 1}}; const auto r0 = placement_order(cores, 1, 0, 2); const auto r1 = placement_order(cores, 1, 1, 2); - BOOST_REQUIRE_EQUAL(r0.size(), 1u); - BOOST_REQUIRE_EQUAL(r1.size(), 1u); - BOOST_CHECK(r0.front() != r1.front()); + REQUIRE((r0.size()) == (1u)); + REQUIRE((r1.size()) == (1u)); + CHECK(r0.front() != r1.front()); } -BOOST_AUTO_TEST_CASE(cpu_topology_policy_domain_major_more_ranks_than_l3) { +TEST_CASE("cpu_topology_policy_domain_major_more_ranks_than_l3") { // 4 cores in 1 L3 domain; 2 ranks each get 2 partitions (flat domain-major arm). // order = [0, 2, 4, 6]; rank0 offset=0 → {0,2}, rank1 offset=2 → {4,6}. const std::vector cores = {{0, 0}, {2, 0}, {4, 0}, {6, 0}}; const auto r0 = placement_order(cores, 2, 0, 2); const auto r1 = placement_order(cores, 2, 1, 2); - BOOST_REQUIRE_EQUAL(r0.size(), 2u); - BOOST_REQUIRE_EQUAL(r1.size(), 2u); + REQUIRE((r0.size()) == (2u)); + REQUIRE((r1.size()) == (2u)); const std::set s0(r0.begin(), r0.end()); const std::set s1(r1.begin(), r1.end()); for (const auto cpu : s1) { - BOOST_CHECK(!s0.contains(cpu)); + CHECK(!s0.contains(cpu)); } } -BOOST_AUTO_TEST_CASE(cpu_topology_policy_insufficient_cores_returns_empty) { +TEST_CASE("cpu_topology_policy_insufficient_cores_returns_empty") { // 2 cores total; 2 ranks × 2 partitions = 4 > 2 ⇒ oversubscription. const std::vector cores = {{0, 0}, {2, 0}}; - BOOST_CHECK(placement_order(cores, 2, 0, 2).empty()); - BOOST_CHECK(placement_order(cores, 2, 1, 2).empty()); + CHECK(placement_order(cores, 2, 0, 2).empty()); + CHECK(placement_order(cores, 2, 1, 2).empty()); // Single rank requesting more cores than exist. - BOOST_CHECK(placement_order(cores, 3, 0, 1).empty()); + CHECK(placement_order(cores, 3, 0, 1).empty()); // Empty core list. - BOOST_CHECK(placement_order({}, 1, 0, 1).empty()); + CHECK(placement_order({}, 1, 0, 1).empty()); } -BOOST_AUTO_TEST_CASE(cpu_topology_policy_singleton_l3_domains) { +TEST_CASE("cpu_topology_policy_singleton_l3_domains") { // 2 cores each in its own singleton domain (no shared L3). // by_domain[0] = {0}, by_domain[1] = {4}; interleaved: 0, 4. const std::vector cores = {{0, 0}, {4, 1}}; const auto order = placement_order(cores, 2, 0, 1); - BOOST_REQUIRE_EQUAL(order.size(), 2u); - BOOST_CHECK_EQUAL(order[0], 0); - BOOST_CHECK_EQUAL(order[1], 4); + REQUIRE((order.size()) == (2u)); + CHECK((order[0]) == (0)); + CHECK((order[1]) == (4)); } -BOOST_AUTO_TEST_CASE(cpu_topology_policy_uneven_domains) { +TEST_CASE("cpu_topology_policy_uneven_domains") { // 3 cores: 2 in domain 0, 1 in domain 1; single rank, 3 partitions. // by_domain[0] = {0, 4}, by_domain[1] = {2}; interleaved: 0, 2, 4. const std::vector cores = {{0, 0}, {2, 1}, {4, 0}}; const auto order = placement_order(cores, 3, 0, 1); - BOOST_REQUIRE_EQUAL(order.size(), 3u); - BOOST_CHECK_EQUAL(order[0], 0); - BOOST_CHECK_EQUAL(order[1], 2); - BOOST_CHECK_EQUAL(order[2], 4); + REQUIRE((order.size()) == (3u)); + CHECK((order[0]) == (0)); + CHECK((order[1]) == (2)); + CHECK((order[2]) == (4)); } /* ── The cgroup-placement classification ──────────────────────────────────── */ -BOOST_AUTO_TEST_CASE(cpu_topology_policy_per_rank_slice_starves_without_collapse) { +TEST_CASE("cpu_topology_policy_per_rank_slice_starves_without_collapse") { // One rank's slice under `srun --cpu-bind=cores`: 2 cores of a 16-core host, one L3 domain. const std::vector slice = {{6, 0}, {7, 0}}; - BOOST_CHECK(placement_order(slice, 2, /*group_index=*/3, /*group_count=*/8).empty()); + CHECK(placement_order(slice, 2, /*group_index=*/3, /*group_count=*/8).empty()); // Collapsed to a single group -- what NodeMask::PerRank does -- the same slice places fully. const auto collapsed = placement_order(slice, 2, /*group_index=*/0, /*group_count=*/1); - BOOST_REQUIRE_EQUAL(collapsed.size(), 2u); - BOOST_CHECK_EQUAL(collapsed[0], 6); - BOOST_CHECK_EQUAL(collapsed[1], 7); + REQUIRE((collapsed.size()) == (2u)); + CHECK((collapsed[0]) == (6)); + CHECK((collapsed[1]) == (7)); } -BOOST_AUTO_TEST_CASE(cpu_topology_policy_private_mask_collapses_even_when_the_split_would_fit) { +TEST_CASE("cpu_topology_policy_private_mask_collapses_even_when_the_split_would_fit") { // 2 ranks x 2 partitions fits these 4 cores, so the collapse is not a fallback: it moves rank 1's cores. const std::vector cores = {{0, 0}, {2, 1}, {4, 0}, {6, 1}}; const auto split = placement_order(cores, 2, /*group_index=*/1, /*group_count=*/2); - BOOST_REQUIRE_EQUAL(split.size(), 2u); - BOOST_CHECK_EQUAL(split[0], 2); - BOOST_CHECK_EQUAL(split[1], 6); + REQUIRE((split.size()) == (2u)); + CHECK((split[0]) == (2)); + CHECK((split[1]) == (6)); // What NodeMask::PerRank now passes: the head of this rank's own interleave over both domains. const auto collapsed = placement_order(cores, 2, /*group_index=*/0, /*group_count=*/1); - BOOST_REQUIRE_EQUAL(collapsed.size(), 2u); - BOOST_CHECK_EQUAL(collapsed[0], 0); - BOOST_CHECK_EQUAL(collapsed[1], 2); + REQUIRE((collapsed.size()) == (2u)); + CHECK((collapsed[0]) == (0)); + CHECK((collapsed[1]) == (2)); } namespace { @@ -259,47 +261,47 @@ auto packed_masks(const std::vector> &pus, size_t words) -> } // namespace -BOOST_AUTO_TEST_CASE(cpu_topology_masks_disjoint_vs_identical) { +TEST_CASE("cpu_topology_masks_disjoint_vs_identical") { constexpr size_t kWords = partition::kAffinityMaskWords; const auto disjoint = packed_masks({{0, 1}, {2, 3}}, kWords); - BOOST_CHECK(partition::masks_are_pairwise_disjoint(disjoint.data(), 2, kWords)); + CHECK(partition::masks_are_pairwise_disjoint(disjoint.data(), 2, kWords)); const auto identical = packed_masks({{0, 1}, {0, 1}}, kWords); - BOOST_CHECK(!partition::masks_are_pairwise_disjoint(identical.data(), 2, kWords)); + CHECK(!partition::masks_are_pairwise_disjoint(identical.data(), 2, kWords)); // Partial overlap: "not private" is conservative, since collapsing points every rank at the same cores. const auto partial = packed_masks({{0, 1}, {1, 2}}, kWords); - BOOST_CHECK(!partition::masks_are_pairwise_disjoint(partial.data(), 2, kWords)); + CHECK(!partition::masks_are_pairwise_disjoint(partial.data(), 2, kWords)); // An unreadable mask arrives empty and must not be read as "disjoint from everything". const auto with_empty = packed_masks({{0, 1}, {}}, kWords); - BOOST_CHECK(!partition::masks_are_pairwise_disjoint(with_empty.data(), 2, kWords)); + CHECK(!partition::masks_are_pairwise_disjoint(with_empty.data(), 2, kWords)); const auto lone = packed_masks({{0, 1}}, kWords); - BOOST_CHECK(!partition::masks_are_pairwise_disjoint(lone.data(), 1, kWords)); + CHECK(!partition::masks_are_pairwise_disjoint(lone.data(), 1, kWords)); const auto four_ok = packed_masks({{0}, {1}, {2}, {3}}, kWords); - BOOST_CHECK(partition::masks_are_pairwise_disjoint(four_ok.data(), 4, kWords)); + CHECK(partition::masks_are_pairwise_disjoint(four_ok.data(), 4, kWords)); const auto four_bad = packed_masks({{0}, {1}, {2}, {1}}, kWords); - BOOST_CHECK(!partition::masks_are_pairwise_disjoint(four_bad.data(), 4, kWords)); + CHECK(!partition::masks_are_pairwise_disjoint(four_bad.data(), 4, kWords)); } -BOOST_AUTO_TEST_CASE(cpu_topology_masks_span_word_boundaries) { +TEST_CASE("cpu_topology_masks_span_word_boundaries") { constexpr size_t kWords = partition::kAffinityMaskWords; // A per-word comparison that forgot to loop would answer from word 0 alone. const auto low_high = packed_masks({{5}, {200}}, kWords); - BOOST_CHECK(partition::masks_are_pairwise_disjoint(low_high.data(), 2, kWords)); + CHECK(partition::masks_are_pairwise_disjoint(low_high.data(), 2, kWords)); const auto both_high = packed_masks({{200}, {200}}, kWords); - BOOST_CHECK(!partition::masks_are_pairwise_disjoint(both_high.data(), 2, kWords)); + CHECK(!partition::masks_are_pairwise_disjoint(both_high.data(), 2, kWords)); const auto late_overlap = packed_masks({{1, 3000}, {2, 3000}}, kWords); - BOOST_CHECK(!partition::masks_are_pairwise_disjoint(late_overlap.data(), 2, kWords)); + CHECK(!partition::masks_are_pairwise_disjoint(late_overlap.data(), 2, kWords)); } -BOOST_AUTO_TEST_CASE(cpu_topology_affinity_mask_covers_enumerated_cores) { +TEST_CASE("cpu_topology_affinity_mask_covers_enumerated_cores") { // The only check that the mask EXCHANGED and the cores PLACED come from one view of the machine. const auto cores = partition::enumerate_physical_cores(); if (cores.empty()) { @@ -309,7 +311,7 @@ BOOST_AUTO_TEST_CASE(cpu_topology_affinity_mask_covers_enumerated_cores) { if (!partition::affinity_mask_words(mine.data(), mine.size())) { // Not a skip: refusal keys on the HIGHEST allowed PU, and core.cpu is each core's LOWEST sibling. std::vector wide(partition::kAffinityMaskWords * 64, 0); - BOOST_REQUIRE(partition::affinity_mask_words(wide.data(), wide.size())); + REQUIRE(partition::affinity_mask_words(wide.data(), wide.size())); size_t highest = 0; for (size_t w = wide.size(); w-- > 0;) { if (wide[w] != 0) { @@ -317,17 +319,17 @@ BOOST_AUTO_TEST_CASE(cpu_topology_affinity_mask_covers_enumerated_cores) { break; } } - BOOST_CHECK_GE(highest, partition::kAffinityMaskWords * 64); + CHECK((highest) >= (partition::kAffinityMaskWords * 64)); return; } size_t set_bits = 0; for (const uint64_t w : mine) { set_bits += static_cast(__builtin_popcountll(w)); } - BOOST_CHECK(set_bits > 0u); + CHECK(set_bits > 0u); for (const auto &core : cores) { const auto pu = static_cast(core.cpu); - BOOST_CHECK((mine[pu / 64] >> (pu % 64)) & 1U); + CHECK((mine[pu / 64] >> (pu % 64)) & 1U); } } @@ -345,13 +347,13 @@ auto confine_to_first(const std::vector &full, size_t k if (sched_setaffinity(0, sizeof(mask), &mask) != 0) { return false; } - BOOST_REQUIRE_EQUAL(partition::enumerate_physical_cores().size(), k); + REQUIRE((partition::enumerate_physical_cores().size()) == (k)); return true; } } // namespace -BOOST_AUTO_TEST_CASE(cpu_topology_per_rank_mask_still_places) { +TEST_CASE("cpu_topology_per_rank_mask_still_places") { const auto full = partition::enumerate_physical_cores(); if (full.empty()) { return; // hwloc loaded no topology: there is no mask to confine to. @@ -370,21 +372,21 @@ BOOST_AUTO_TEST_CASE(cpu_topology_per_rank_mask_still_places) { if (sets.empty() && empty_placement_is_licensed()) { return; } - BOOST_REQUIRE_EQUAL(sets.size(), k); + REQUIRE((sets.size()) == (k)); for (const auto &set : sets) { // Never pin outside the mask the launcher gave us. bool inside = false; for (size_t i = 0; i < k; ++i) { inside = inside || set.pu == full[i].cpu; } - BOOST_CHECK(inside); + CHECK(inside); } if (k == 2) { - BOOST_CHECK(sets[0].pu != sets[1].pu); + CHECK(sets[0].pu != sets[1].pu); } } -BOOST_AUTO_TEST_CASE(cpu_topology_shared_mask_keeps_co_located_ranks_disjoint) { +TEST_CASE("cpu_topology_shared_mask_keeps_co_located_ranks_disjoint") { const auto full = partition::enumerate_physical_cores(); if (full.size() < 2) { return; // two ranks cannot hold disjoint cores when there is only one @@ -409,12 +411,12 @@ BOOST_AUTO_TEST_CASE(cpu_topology_shared_mask_keeps_co_located_ranks_disjoint) { if (rank0.empty() && rank1.empty() && empty_placement_is_licensed()) { return; } - BOOST_REQUIRE_EQUAL(rank0.size(), per_rank); - BOOST_REQUIRE_EQUAL(rank1.size(), per_rank); + REQUIRE((rank0.size()) == (per_rank)); + REQUIRE((rank1.size()) == (per_rank)); for (const auto &a : rank0) { for (const auto &b : rank1) { // Two ranks on one core starve each other: busy-polling collectives against barrier spins. - BOOST_CHECK(a.pu != b.pu); + CHECK(a.pu != b.pu); } } } diff --git a/cpp/tests/ctor_validation_tests.cpp b/cpp/tests/ctor_validation_tests.cpp index 2eddcd19..bbbec258 100644 --- a/cpp/tests/ctor_validation_tests.cpp +++ b/cpp/tests/ctor_validation_tests.cpp @@ -14,7 +14,9 @@ // The MonomialPropagator throw paths that define the public contract. -#include +#include +#include +#include #include #include @@ -57,132 +59,132 @@ auto make(const OperatorDict &op, } } // namespace -BOOST_AUTO_TEST_CASE(ctor_accepts_valid_config) { - BOOST_CHECK_NO_THROW(make(OperatorDict{})); +TEST_CASE("ctor_accepts_valid_config") { + CHECK_NOTHROW(make(OperatorDict{})); } -BOOST_AUTO_TEST_CASE(ctor_logical_num_modes_out_of_range_throws) { - BOOST_CHECK_THROW(make(OperatorDict{}, - 2 * N, - std::nullopt, - std::nullopt, - CutoffType::Length, - std::nullopt, - /*logical=*/0), - std::runtime_error); - BOOST_CHECK_THROW(make(OperatorDict{}, - 2 * N, - std::nullopt, - std::nullopt, - CutoffType::Length, - std::nullopt, - /*logical=*/N + 1), - std::runtime_error); +TEST_CASE("ctor_logical_num_modes_out_of_range_throws") { + CHECK_THROWS_AS(make(OperatorDict{}, + 2 * N, + std::nullopt, + std::nullopt, + CutoffType::Length, + std::nullopt, + /*logical=*/0), + std::runtime_error); + CHECK_THROWS_AS(make(OperatorDict{}, + 2 * N, + std::nullopt, + std::nullopt, + CutoffType::Length, + std::nullopt, + /*logical=*/N + 1), + std::runtime_error); } -BOOST_AUTO_TEST_CASE(ctor_pauli_requires_support_cutoff_throws) { +TEST_CASE("ctor_pauli_requires_support_cutoff_throws") { // Length has no Pauli-weight meaning. - BOOST_CHECK_THROW( + CHECK_THROWS_AS( make(OperatorDict{}, 2 * N, std::nullopt, std::nullopt, CutoffType::Length, std::nullopt, N, Basis::Pauli), std::invalid_argument); - BOOST_CHECK_NO_THROW( + CHECK_NOTHROW( make(OperatorDict{}, 2 * N, std::nullopt, std::nullopt, CutoffType::Support, std::nullopt, N, Basis::Pauli)); } -BOOST_AUTO_TEST_CASE(ctor_pauli_forbids_basis_change_throws) { +TEST_CASE("ctor_pauli_forbids_basis_change_throws") { const std::vector some_basis(2 * N, VecZ{0}); - BOOST_CHECK_THROW( + CHECK_THROWS_AS( make(OperatorDict{}, 2 * N, std::nullopt, std::nullopt, CutoffType::Support, some_basis, N, Basis::Pauli), std::invalid_argument); } -BOOST_AUTO_TEST_CASE(ctor_upper_atol_below_lower_atol_throws) { - BOOST_CHECK_THROW(make(OperatorDict{}, 2 * N, /*lower=*/1e-6, /*upper=*/1e-8), std::runtime_error); - BOOST_CHECK_NO_THROW(make(OperatorDict{}, 2 * N, /*lower=*/1e-8, /*upper=*/1e-6)); +TEST_CASE("ctor_upper_atol_below_lower_atol_throws") { + CHECK_THROWS_AS(make(OperatorDict{}, 2 * N, /*lower=*/1e-6, /*upper=*/1e-8), std::runtime_error); + CHECK_NOTHROW(make(OperatorDict{}, 2 * N, /*lower=*/1e-8, /*upper=*/1e-6)); } -BOOST_AUTO_TEST_CASE(ctor_operator_index_out_of_range_throws) { +TEST_CASE("ctor_operator_index_out_of_range_throws") { OperatorDict op; op[VecZ{20}] = std::complex(1.0, 0.0); // 20 >= 2*logical (=16) - BOOST_CHECK_THROW(make(op), std::runtime_error); + CHECK_THROWS_AS(make(op), std::runtime_error); } // A gate generator index outside the system must throw, not underflow 2*NumModes-1-index into an // out-of-bounds Bitset::set. -BOOST_AUTO_TEST_CASE(build_graph_generator_index_out_of_range_throws) { +TEST_CASE("build_graph_generator_index_out_of_range_throws") { OperatorDict op; op[VecZ{0, 1}] = std::complex(0.0, 1.0); auto sim = make(op); // 2*logical_num_modes == 16, so slot 20 is outside this system. - BOOST_CHECK_THROW(sim.build_graph({VecZ{20, 21}}, VecZ{0}, VecD{1.0}), std::runtime_error); - BOOST_CHECK_NO_THROW(sim.build_graph({VecZ{0, 3}}, VecZ{0}, VecD{1.0})); + CHECK_THROWS_AS(sim.build_graph({VecZ{20, 21}}, VecZ{0}, VecD{1.0}), std::runtime_error); + CHECK_NOTHROW(sim.build_graph({VecZ{0, 3}}, VecZ{0}, VecD{1.0})); } -BOOST_AUTO_TEST_CASE(generator_index_bound_is_logical_not_storage) { +TEST_CASE("generator_index_bound_is_logical_not_storage") { OperatorDict op; op[VecZ{0, 1}] = std::complex(0.0, 1.0); auto sim = make(op, 2 * N, std::nullopt, std::nullopt, CutoffType::Length, std::nullopt, /*logical=*/4); // 2*logical == 8 <= slot 9 < 2*N == 16: inside the storage, outside the system. - BOOST_CHECK_THROW(sim.build_graph({VecZ{9}}, VecZ{0}, VecD{1.0}), std::runtime_error); + CHECK_THROWS_AS(sim.build_graph({VecZ{9}}, VecZ{0}, VecD{1.0}), std::runtime_error); } -BOOST_AUTO_TEST_CASE(only_rotate_len_k_build_graph_validation_matches_python_contract) { +TEST_CASE("only_rotate_len_k_build_graph_validation_matches_python_contract") { auto sim = make(OperatorDict{}); - BOOST_CHECK_THROW(sim.build_graph({VecZ{0, 1}}, VecZ{0}, VecD{1.0}, std::nullopt, std::nullopt, /*k=*/0u), - std::runtime_error); - BOOST_CHECK_NO_THROW(sim.build_graph({VecZ{0, 1}}, VecZ{0}, VecD{1.0}, std::nullopt, std::nullopt, std::nullopt)); + CHECK_THROWS_AS(sim.build_graph({VecZ{0, 1}}, VecZ{0}, VecD{1.0}, std::nullopt, std::nullopt, /*k=*/0u), + std::runtime_error); + CHECK_NOTHROW(sim.build_graph({VecZ{0, 1}}, VecZ{0}, VecD{1.0}, std::nullopt, std::nullopt, std::nullopt)); auto logical_bound = make(OperatorDict{}, 2 * N, std::nullopt, std::nullopt, CutoffType::Length, std::nullopt, 4); - BOOST_CHECK_THROW(logical_bound.build_graph({VecZ{0, 1}}, VecZ{0}, VecD{1.0}, std::nullopt, std::nullopt, 9u), - std::runtime_error); - BOOST_CHECK_NO_THROW(logical_bound.build_graph({VecZ{0, 1}}, VecZ{0}, VecD{1.0}, std::nullopt, std::nullopt, 8u)); + CHECK_THROWS_AS(logical_bound.build_graph({VecZ{0, 1}}, VecZ{0}, VecD{1.0}, std::nullopt, std::nullopt, 9u), + std::runtime_error); + CHECK_NOTHROW(logical_bound.build_graph({VecZ{0, 1}}, VecZ{0}, VecD{1.0}, std::nullopt, std::nullopt, 8u)); } -BOOST_AUTO_TEST_CASE(only_rotate_len_k_propagate_validation_matches_python_contract) { +TEST_CASE("only_rotate_len_k_propagate_validation_matches_python_contract") { auto sim = make(OperatorDict{}); - BOOST_CHECK_THROW(sim.propagate({VecZ{0, 1}}, VecZ{0}, VecD{1.0}, VecD{0.5}, /*k=*/0u), std::runtime_error); - BOOST_CHECK_NO_THROW(sim.propagate({VecZ{0, 1}}, VecZ{0}, VecD{1.0}, VecD{0.5}, std::nullopt)); + CHECK_THROWS_AS(sim.propagate({VecZ{0, 1}}, VecZ{0}, VecD{1.0}, VecD{0.5}, /*k=*/0u), std::runtime_error); + CHECK_NOTHROW(sim.propagate({VecZ{0, 1}}, VecZ{0}, VecD{1.0}, VecD{0.5}, std::nullopt)); auto logical_bound = make(OperatorDict{}, 2 * N, std::nullopt, std::nullopt, CutoffType::Length, std::nullopt, 4); - BOOST_CHECK_THROW(logical_bound.propagate({VecZ{0, 1}}, VecZ{0}, VecD{1.0}, VecD{0.5}, 9u), std::runtime_error); - BOOST_CHECK_NO_THROW(logical_bound.propagate({VecZ{0, 1}}, VecZ{0}, VecD{1.0}, VecD{0.5}, 8u)); + CHECK_THROWS_AS(logical_bound.propagate({VecZ{0, 1}}, VecZ{0}, VecD{1.0}, VecD{0.5}, 9u), std::runtime_error); + CHECK_NOTHROW(logical_bound.propagate({VecZ{0, 1}}, VecZ{0}, VecD{1.0}, VecD{0.5}, 8u)); } // The update_* setters must not write straight through to regenerate_cutoff_fn_(). -BOOST_AUTO_TEST_CASE(setters_enforce_the_constructor_invariants) { +TEST_CASE("setters_enforce_the_constructor_invariants") { auto pauli = make(OperatorDict{}, 2 * N, std::nullopt, std::nullopt, CutoffType::Support, std::nullopt, N, Basis::Pauli); - BOOST_CHECK_THROW(pauli.update_cutoff_type(CutoffType::Length), std::invalid_argument); - BOOST_CHECK_THROW(pauli.update_basis_change(std::vector(2 * N, VecZ{0})), std::invalid_argument); + CHECK_THROWS_AS(pauli.update_cutoff_type(CutoffType::Length), std::invalid_argument); + CHECK_THROWS_AS(pauli.update_basis_change(std::vector(2 * N, VecZ{0})), std::invalid_argument); auto majorana = make(OperatorDict{}); // Too few rows: regenerate_cutoff_fn_ indexes [0, 2*logical_num_modes) unconditionally. - BOOST_CHECK_THROW(majorana.update_basis_change(std::vector{VecZ{0}}), std::invalid_argument); - BOOST_CHECK_THROW(majorana.update_basis_change(std::vector(2 * N, VecZ{2 * N})), std::runtime_error); + CHECK_THROWS_AS(majorana.update_basis_change(std::vector{VecZ{0}}), std::invalid_argument); + CHECK_THROWS_AS(majorana.update_basis_change(std::vector(2 * N, VecZ{2 * N})), std::runtime_error); std::vector identity(2 * N); for (size_t i = 0; i < identity.size(); ++i) { identity[i] = VecZ{i}; } - BOOST_CHECK_NO_THROW(majorana.update_basis_change(identity)); + CHECK_NOTHROW(majorana.update_basis_change(identity)); } -BOOST_FIXTURE_TEST_CASE(propagate_on_nonempty_graph_throws, ExampleDataFix) { +TEST_CASE_METHOD(ExampleDataFix, "propagate_on_nonempty_graph_throws") { auto sim = build_simulator(data, SimulatorConfig{}); sim.build_graph(data.majoranas, data.param_inds, data.gen_coeffs); - BOOST_REQUIRE(sim.graph_layers() > 0); - BOOST_CHECK_THROW(sim.propagate(data.majoranas, data.param_inds, data.gen_coeffs, data.parameters), - std::runtime_error); + REQUIRE(sim.graph_layers() > 0); + CHECK_THROWS_AS(sim.propagate(data.majoranas, data.param_inds, data.gen_coeffs, data.parameters), + std::runtime_error); } // Pins MPGraph::get_layer's checked_layer_offset throw site. -BOOST_FIXTURE_TEST_CASE(graph_get_layer_out_of_range_throws, ExampleDataFix) { +TEST_CASE_METHOD(ExampleDataFix, "graph_get_layer_out_of_range_throws") { auto sim = build_simulator(data, SimulatorConfig{}); sim.build_graph(data.majoranas, data.param_inds, data.gen_coeffs); const auto &graph = sim.graph(); const size_t n_layers = graph.layers(); - BOOST_REQUIRE(n_layers > 0); - BOOST_CHECK_NO_THROW((void)graph.get_layer(0)); - BOOST_CHECK_THROW((void)graph.get_layer(n_layers), std::exception); + REQUIRE(n_layers > 0); + CHECK_NOTHROW((void)graph.get_layer(0)); + CHECK_THROWS_AS((void)graph.get_layer(n_layers), std::exception); } diff --git a/cpp/tests/env_config_tests.cpp b/cpp/tests/env_config_tests.cpp index 77ff0955..66d537cc 100644 --- a/cpp/tests/env_config_tests.cpp +++ b/cpp/tests/env_config_tests.cpp @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include +#include +#include +#include #include @@ -21,48 +23,50 @@ using monoprop::config::detail::parse_flag; using monoprop::config::detail::parse_positive_int; -BOOST_AUTO_TEST_CASE(env_config_parse_flag_default_when_unset_or_empty) { - BOOST_CHECK_EQUAL(parse_flag(nullptr, true), true); - BOOST_CHECK_EQUAL(parse_flag(nullptr, false), false); - BOOST_CHECK_EQUAL(parse_flag("", true), true); - BOOST_CHECK_EQUAL(parse_flag("", false), false); +TEST_CASE("env_config_parse_flag_default_when_unset_or_empty") { + CHECK((parse_flag(nullptr, true)) == (true)); + CHECK((parse_flag(nullptr, false)) == (false)); + CHECK((parse_flag("", true)) == (true)); + CHECK((parse_flag("", false)) == (false)); } -BOOST_AUTO_TEST_CASE(env_config_parse_flag_falsey_first_char) { +TEST_CASE("env_config_parse_flag_falsey_first_char") { // Only the first character decides, so "0abc" is falsey too. for (const char *v : {"0", "f", "F", "n", "N"}) { - BOOST_CHECK_MESSAGE(parse_flag(v, true) == false, v); + INFO(v); + CHECK(parse_flag(v, true) == false); } - BOOST_CHECK_EQUAL(parse_flag("0abc", true), false); + CHECK((parse_flag("0abc", true)) == (false)); } -BOOST_AUTO_TEST_CASE(env_config_parse_flag_truthy_first_char) { +TEST_CASE("env_config_parse_flag_truthy_first_char") { for (const char *v : {"1", "t", "T", "y", "Y", "on", "true", "anything"}) { - BOOST_CHECK_MESSAGE(parse_flag(v, false) == true, v); + INFO(v); + CHECK(parse_flag(v, false) == true); } } -BOOST_AUTO_TEST_CASE(env_config_parse_positive_int_null_and_malformed) { - BOOST_CHECK(parse_positive_int(nullptr) == std::nullopt); - BOOST_CHECK(parse_positive_int("") == std::nullopt); - BOOST_CHECK(parse_positive_int("abc") == std::nullopt); - BOOST_CHECK(parse_positive_int("12x") == std::nullopt); // trailing junk rejects, not a partial 12 - BOOST_CHECK(parse_positive_int(" ") == std::nullopt); // strtol consumes ws, then end == text +TEST_CASE("env_config_parse_positive_int_null_and_malformed") { + CHECK(parse_positive_int(nullptr) == std::nullopt); + CHECK(parse_positive_int("") == std::nullopt); + CHECK(parse_positive_int("abc") == std::nullopt); + CHECK(parse_positive_int("12x") == std::nullopt); // trailing junk rejects, not a partial 12 + CHECK(parse_positive_int(" ") == std::nullopt); // strtol consumes ws, then end == text } -BOOST_AUTO_TEST_CASE(env_config_parse_positive_int_range) { - BOOST_CHECK(parse_positive_int("0") == std::nullopt); - BOOST_CHECK(parse_positive_int("-5") == std::nullopt); - BOOST_CHECK(parse_positive_int("1000001") == std::nullopt); // above the 1e6 ceiling - BOOST_CHECK(parse_positive_int("1") == std::optional(1)); - BOOST_CHECK(parse_positive_int("42") == std::optional(42)); - BOOST_CHECK(parse_positive_int("1000000") == std::optional(1'000'000)); // inclusive upper bound +TEST_CASE("env_config_parse_positive_int_range") { + CHECK(parse_positive_int("0") == std::nullopt); + CHECK(parse_positive_int("-5") == std::nullopt); + CHECK(parse_positive_int("1000001") == std::nullopt); // above the 1e6 ceiling + CHECK(parse_positive_int("1") == std::optional(1)); + CHECK(parse_positive_int("42") == std::optional(42)); + CHECK(parse_positive_int("1000000") == std::optional(1'000'000)); // inclusive upper bound } -BOOST_AUTO_TEST_CASE(env_config_settings_cached_singleton) { +TEST_CASE("env_config_settings_cached_singleton") { const auto &a = monoprop::config::get(); const auto &b = monoprop::config::get(); - BOOST_CHECK_EQUAL(&a, &b); + CHECK((&a) == (&b)); // Touch a field so the Settings aggregate is actually read. - BOOST_CHECK(a.partition_pinning == true || a.partition_pinning == false); + CHECK((a.partition_pinning == true || a.partition_pinning == false)); } diff --git a/cpp/tests/evolution_detail_tests.cpp b/cpp/tests/evolution_detail_tests.cpp index 39555948..a25d19d2 100644 --- a/cpp/tests/evolution_detail_tests.cpp +++ b/cpp/tests/evolution_detail_tests.cpp @@ -14,7 +14,9 @@ // MatchedEpochSet and CutoffContext driven directly, not through build_layer. -#include +#include +#include +#include #include #include @@ -27,92 +29,92 @@ using namespace monoprop; using monoprop::detail::CutoffContext; using monoprop::detail::MatchedEpochSet; -BOOST_AUTO_TEST_CASE(matched_epoch_begin_gate_clears_all) { +TEST_CASE("matched_epoch_begin_gate_clears_all") { MatchedEpochSet set; set.begin_gate(5); set.mark(2); set.mark(4); - BOOST_TEST(set.is_marked(2)); - BOOST_TEST(set.is_marked(4)); - BOOST_TEST(!set.is_marked(0)); + CHECK(set.is_marked(2)); + CHECK(set.is_marked(4)); + CHECK(!set.is_marked(0)); set.begin_gate(5); - BOOST_TEST(!set.is_marked(2)); - BOOST_TEST(!set.is_marked(4)); + CHECK(!set.is_marked(2)); + CHECK(!set.is_marked(4)); set.mark(0); - BOOST_TEST(set.is_marked(0)); - BOOST_TEST(!set.is_marked(2)); + CHECK(set.is_marked(0)); + CHECK(!set.is_marked(2)); } // Growing the operator only appends to the tail; old slots stay cleared and new slots are usable. -BOOST_AUTO_TEST_CASE(matched_epoch_tail_grow) { +TEST_CASE("matched_epoch_tail_grow") { MatchedEpochSet set; set.begin_gate(4); set.mark(3); - BOOST_TEST(set.is_marked(3)); + CHECK(set.is_marked(3)); set.begin_gate(8); - BOOST_TEST(!set.is_marked(3)); + CHECK(!set.is_marked(3)); set.mark(7); - BOOST_TEST(set.is_marked(7)); - BOOST_TEST(!set.is_marked(3)); + CHECK(set.is_marked(7)); + CHECK(!set.is_marked(3)); } // When the epoch counter saturates uint32_t, begin_gate zero-fills and restarts so marks stay correct. -BOOST_AUTO_TEST_CASE(matched_epoch_u32_wrap_resets) { +TEST_CASE("matched_epoch_u32_wrap_resets") { MatchedEpochSet set; set.begin_gate(4); // allocate the backing array // Force the counter to the wrap boundary; a stale slot still equals the pre-wrap counter. set.cur_ = std::numeric_limits::max(); set.mark(1); - BOOST_TEST(set.is_marked(1)); + CHECK(set.is_marked(1)); set.begin_gate(4); // triggers the fill(0) + cur_ = 0 -> ++cur_ = 1 reset - BOOST_TEST(set.cur_ == 1U); - BOOST_TEST(!set.is_marked(1)); + CHECK(set.cur_ == 1U); + CHECK(!set.is_marked(1)); set.mark(2); - BOOST_TEST(set.is_marked(2)); + CHECK(set.is_marked(2)); } -BOOST_AUTO_TEST_CASE(cutoff_context_abs_coeff_for) { +TEST_CASE("cutoff_context_abs_coeff_for") { const VecD coeffs{-3.0, 2.0, 0.0}; CutoffContext off; // use_coeff_checks defaults false - BOOST_TEST(off.abs_coeff_for(0, coeffs) == 0.0); + CHECK(off.abs_coeff_for(0, coeffs) == 0.0); CutoffContext on; on.use_coeff_checks = true; - BOOST_TEST(on.abs_coeff_for(0, coeffs) == 3.0); - BOOST_TEST(on.abs_coeff_for(1, coeffs) == 2.0); - BOOST_TEST(on.abs_coeff_for(3, coeffs) == 0.0); // out of range -> 0 + CHECK(on.abs_coeff_for(0, coeffs) == 3.0); + CHECK(on.abs_coeff_for(1, coeffs) == 2.0); + CHECK(on.abs_coeff_for(3, coeffs) == 0.0); // out of range -> 0 } // is_above_upper is the rescue predicate: enabled AND |sin|·|coeff| >= upper_atol (inclusive). -BOOST_AUTO_TEST_CASE(cutoff_context_is_above_upper) { +TEST_CASE("cutoff_context_is_above_upper") { CutoffContext ctx; ctx.abs_sin_val = 0.5; ctx.check_upper_atol = false; - BOOST_TEST(!ctx.is_above_upper(100.0)); + CHECK(!ctx.is_above_upper(100.0)); ctx.check_upper_atol = true; ctx.upper_atol_value = 1.0; - BOOST_TEST(ctx.is_above_upper(2.0)); // 0.5*2.0 == 1.0 -> boundary inclusive - BOOST_TEST(ctx.is_above_upper(4.0)); - BOOST_TEST(!ctx.is_above_upper(1.0)); + CHECK(ctx.is_above_upper(2.0)); // 0.5*2.0 == 1.0 -> boundary inclusive + CHECK(ctx.is_above_upper(4.0)); + CHECK(!ctx.is_above_upper(1.0)); } // is_below_sin is the lower-atol drop predicate: enabled AND |sin|·|coeff| <= atol (inclusive). -BOOST_AUTO_TEST_CASE(cutoff_context_is_below_sin) { +TEST_CASE("cutoff_context_is_below_sin") { CutoffContext ctx; ctx.abs_sin_val = 2.0; ctx.check_atol = false; - BOOST_TEST(!ctx.is_below_sin(0.0)); + CHECK(!ctx.is_below_sin(0.0)); ctx.check_atol = true; ctx.atol_value = 1.0; - BOOST_TEST(ctx.is_below_sin(0.5)); // 2.0*0.5 == 1.0 -> boundary inclusive - BOOST_TEST(ctx.is_below_sin(0.1)); - BOOST_TEST(!ctx.is_below_sin(1.0)); + CHECK(ctx.is_below_sin(0.5)); // 2.0*0.5 == 1.0 -> boundary inclusive + CHECK(ctx.is_below_sin(0.1)); + CHECK(!ctx.is_below_sin(1.0)); } diff --git a/cpp/tests/exact_upper_atol_rescue.cpp b/cpp/tests/exact_upper_atol_rescue.cpp index 3243d965..bfb81c7b 100644 --- a/cpp/tests/exact_upper_atol_rescue.cpp +++ b/cpp/tests/exact_upper_atol_rescue.cpp @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include +#include +#include +#include #include #include @@ -58,16 +60,17 @@ auto evaluate_zero_cutoff_full_rescue_energy(MonomialPropagator& simul } // namespace // One test per (fixture, comm) so a failure pinpoints the configuration. +#define STRINGIFY_IMPL(Token) #Token +#define STRINGIFY(Token) STRINGIFY_IMPL(Token) #define MAKE_ZERO_CUTOFF_RESCUE_TEST(NAME, FixtureType, CommToken) \ - BOOST_FIXTURE_TEST_CASE(NAME##_##CommToken, FixtureType) { \ + TEST_CASE_METHOD(FixtureType, STRINGIFY(NAME) "_" STRINGIFY(CommToken)) { \ MPI_Comm comm = (CommMode::CommToken == CommMode::Self) ? MPI_COMM_SELF : MPI_COMM_WORLD; \ if (CommMode::CommToken == CommMode::World && mpi::size(comm) == 1) { \ - BOOST_TEST_MESSAGE("Skipping multi-rank scenario for " #NAME " (world size=1)"); \ - return; \ + SKIP("Skipping multi-rank scenario for " #NAME " (world size=1)"); \ } \ auto simulator = build_zero_cutoff_full_rescue(data, comm); \ const double energy = evaluate_zero_cutoff_full_rescue_energy(simulator, data); \ - BOOST_CHECK_SMALL(std::abs(energy - data.actual_expval), kEnergyAtol); \ + CHECK_THAT(energy - data.actual_expval, Catch::Matchers::WithinAbs(0.0, kEnergyAtol)); \ } MAKE_ZERO_CUTOFF_RESCUE_TEST(zero_cutoff_upper_atol_zero_is_exact, ExampleDataFix, Self) @@ -77,3 +80,5 @@ MAKE_ZERO_CUTOFF_RESCUE_TEST(zero_cutoff_upper_atol_zero_is_exact_lih, LihFixtur MAKE_ZERO_CUTOFF_RESCUE_TEST(zero_cutoff_upper_atol_zero_is_exact_lih, LihFixture, World) #undef MAKE_ZERO_CUTOFF_RESCUE_TEST +#undef STRINGIFY +#undef STRINGIFY_IMPL diff --git a/cpp/tests/fused_cos_sweep_tests.cpp b/cpp/tests/fused_cos_sweep_tests.cpp index a8d36ea2..b963923f 100644 --- a/cpp/tests/fused_cos_sweep_tests.cpp +++ b/cpp/tests/fused_cos_sweep_tests.cpp @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include +#include +#include +#include #include @@ -50,30 +52,31 @@ auto graph_energy(const CaseData &data, const SimulatorConfig &cfg) -> double { void check_agreement(const CaseData &data, const SimulatorConfig &cfg, const char *label) { const double inplace = inplace_energy(data, cfg); const double graph = graph_energy(data, cfg); - BOOST_TEST_CONTEXT(label << " inplace=" << inplace << " graph=" << graph) { - BOOST_CHECK_SMALL(inplace - graph, kAgreeAtol); - BOOST_CHECK_SMALL(inplace - data.actual_expval, kExactAtol); + { + INFO(label << " inplace=" << inplace << " graph=" << graph); + CHECK_THAT(inplace - graph, Catch::Matchers::WithinAbs(0.0, kAgreeAtol)); + CHECK_THAT(inplace - data.actual_expval, Catch::Matchers::WithinAbs(0.0, kExactAtol)); } } } // namespace -BOOST_FIXTURE_TEST_CASE(fused_sweep_matches_graph_replay_heisenberg, ExampleDataFix) { +TEST_CASE_METHOD(ExampleDataFix, "fused_sweep_matches_graph_replay_heisenberg") { check_agreement(data, SimulatorConfig{}, "heisenberg"); } // lower_atol active: the sin gate reads the pre-cos value, so the in-place store that follows must // not change which terms are emitted. -BOOST_FIXTURE_TEST_CASE(fused_sweep_matches_graph_replay_heisenberg_atol, ExampleDataFix) { +TEST_CASE_METHOD(ExampleDataFix, "fused_sweep_matches_graph_replay_heisenberg_atol") { check_agreement(data, SimulatorConfig{.atol = 1e-10}, "heisenberg atol=1e-10"); } // Schrödinger: fresh inserts are born after the sweep with a nonzero coeff, so the apply's insert arm // must fold the gate's cos into those slots itself. -BOOST_FIXTURE_TEST_CASE(fused_sweep_matches_graph_replay_schrodinger, ExampleDataFix) { +TEST_CASE_METHOD(ExampleDataFix, "fused_sweep_matches_graph_replay_schrodinger") { check_agreement(data, SimulatorConfig{.schrodinger_cutoff = 2 * n_modes}, "schrodinger"); } -BOOST_FIXTURE_TEST_CASE(fused_sweep_matches_graph_replay_schrodinger_atol, ExampleDataFix) { +TEST_CASE_METHOD(ExampleDataFix, "fused_sweep_matches_graph_replay_schrodinger_atol") { check_agreement(data, SimulatorConfig{.schrodinger_cutoff = 2 * n_modes, .atol = 1e-10}, "schrodinger atol=1e-10"); } diff --git a/cpp/tests/fused_query_codec_tests.cpp b/cpp/tests/fused_query_codec_tests.cpp index 2eb69ab0..1eabac8c 100644 --- a/cpp/tests/fused_query_codec_tests.cpp +++ b/cpp/tests/fused_query_codec_tests.cpp @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include +#include +#include +#include #include #include @@ -48,7 +50,7 @@ auto make_mono(size_t r) -> Monomial { return m; } -BOOST_AUTO_TEST_CASE(fused_record_roundtrip_exact) { +TEST_CASE("fused_record_roundtrip_exact") { const std::vector phases = {1, -1, 1, -1, 1, 1, -1}; const std::vector values = { 0.0, @@ -67,27 +69,27 @@ BOOST_AUTO_TEST_CASE(fused_record_roundtrip_exact) { monos[r] = make_mono(r); query_push(plain, monos[r], phases[r]); } - BOOST_REQUIRE_EQUAL(plain.size(), nq * kQueryWords); + REQUIRE((plain.size()) == (nq * kQueryWords)); VecZ fused; build_fused_query_value(plain, values, fused); - BOOST_REQUIRE_EQUAL(fused.size(), nq * kQueryWordsFused); + REQUIRE((fused.size()) == (nq * kQueryWordsFused)); for (size_t q = 0; q < nq; ++q) { Monomial m_out; int ph_out = 0; query_read>(fused, q, m_out, ph_out); - BOOST_CHECK(m_out == monos[q]); - BOOST_CHECK_EQUAL(ph_out, phases[q]); + CHECK(m_out == monos[q]); + CHECK((ph_out) == (phases[q])); // Compare the raw payload, so -0.0 and denormals stay distinguished from 0.0. const double v_out = query_value(fused, q); - BOOST_CHECK(std::memcmp(&v_out, &values[q], sizeof(double)) == 0); + CHECK(std::memcmp(&v_out, &values[q], sizeof(double)) == 0); } } // Reusing `out` across calls must leak no stale words: capacity is a high-water mark, size is exact. // This is the reuse pattern LayerBuildEngine::combined_qv_ relies on gate to gate. -BOOST_AUTO_TEST_CASE(fused_buffer_reuse_shrinks_logical_size) { +TEST_CASE("fused_buffer_reuse_shrinks_logical_size") { VecZ plain_big; std::vector vbig; for (size_t r = 0; r < 32; ++r) { @@ -104,21 +106,21 @@ BOOST_AUTO_TEST_CASE(fused_buffer_reuse_shrinks_logical_size) { query_push(plain_small, make_mono(100 + r), 1); } build_fused_query_value(plain_small, vsmall, out); - BOOST_CHECK_EQUAL(out.size(), vsmall.size() * kQueryWordsFused); - BOOST_CHECK_GE(out.capacity(), cap_after_big); + CHECK((out.size()) == (vsmall.size() * kQueryWordsFused)); + CHECK((out.capacity()) >= (cap_after_big)); for (size_t q = 0; q < vsmall.size(); ++q) { const double v_out = query_value(out, q); - BOOST_CHECK(std::memcmp(&v_out, &vsmall[q], sizeof(double)) == 0); + CHECK(std::memcmp(&v_out, &vsmall[q], sizeof(double)) == 0); } } // Empty input arises for the self slot, which resolve_self_queries clears before the exchange. -BOOST_AUTO_TEST_CASE(fused_empty_input) { +TEST_CASE("fused_empty_input") { VecZ empty; std::vector no_values; VecZ out{1, 2, 3}; // pre-dirtied; build must clear it build_fused_query_value(empty, no_values, out); - BOOST_CHECK(out.empty()); + CHECK(out.empty()); } } // namespace diff --git a/cpp/tests/gate_boundaries.cpp b/cpp/tests/gate_boundaries.cpp index 0fa4b601..61d0a914 100644 --- a/cpp/tests/gate_boundaries.cpp +++ b/cpp/tests/gate_boundaries.cpp @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include +#include +#include +#include #include #include @@ -45,24 +47,24 @@ auto make_sim() -> MonomialPropagator { } // namespace -BOOST_AUTO_TEST_CASE(n_gates_defaults_to_one_per_generator) { +TEST_CASE("n_gates_defaults_to_one_per_generator") { auto sim = make_sim(); const std::vector monos{{0}, {1}, {2}}; sim.build_graph(monos, VecZ{0, 1, 2}, VecD{1.0, 1.0, 1.0}); - BOOST_TEST(sim.graph_layers() == 3u); - BOOST_TEST(sim.n_gates() == sim.graph_layers()); + CHECK(sim.graph_layers() == 3u); + CHECK(sim.n_gates() == sim.graph_layers()); } -BOOST_AUTO_TEST_CASE(gate_indices_group_layers) { +TEST_CASE("gate_indices_group_layers") { auto sim = make_sim(); const std::vector monos{{0}, {1}, {2}}; // Two monomials belong to gate 0, one to gate 1. sim.build_graph(monos, VecZ{0, 1, 2}, VecD{1.0, 1.0, 1.0}, VecZ{0, 0, 1}); - BOOST_TEST(sim.graph_layers() == 3u); - BOOST_TEST(sim.n_gates() == 2u); + CHECK(sim.graph_layers() == 3u); + CHECK(sim.n_gates() == 2u); } -BOOST_AUTO_TEST_CASE(set_parameter_mapping_per_gate_ties_layers) { +TEST_CASE("set_parameter_mapping_per_gate_ties_layers") { auto sim = make_sim(); const std::vector monos{{0}, {1}, {2}}; sim.build_graph(monos, VecZ{0, 1, 2}, VecD{1.0, 1.0, 1.0}, VecZ{0, 0, 1}); @@ -70,8 +72,8 @@ BOOST_AUTO_TEST_CASE(set_parameter_mapping_per_gate_ties_layers) { // Length n_gates (2) selects the per-gate branch; both gates tied to one angle. sim.set_parameter_mapping(VecZ{0, 0}); auto per_layer = sim.parameter_mapping(); - BOOST_TEST(per_layer.size() == 3u); - BOOST_TEST(std::ranges::all_of(per_layer, [](size_t p) { return p == 0; })); + CHECK(per_layer.size() == 3u); + CHECK(std::ranges::all_of(per_layer, [](size_t p) { return p == 0; })); // Distinct angles: gate 0's two layers share one param, gate 1's layer gets the other -- counted // rather than compared positionally, so the check holds regardless of layer ordering. @@ -79,24 +81,24 @@ BOOST_AUTO_TEST_CASE(set_parameter_mapping_per_gate_ties_layers) { per_layer = sim.parameter_mapping(); const auto zeros = std::ranges::count(per_layer, 0u); const auto ones = std::ranges::count(per_layer, 1u); - BOOST_TEST(zeros == 2); - BOOST_TEST(ones == 1); + CHECK(zeros == 2); + CHECK(ones == 1); } -BOOST_AUTO_TEST_CASE(set_parameter_mapping_per_layer_still_works) { +TEST_CASE("set_parameter_mapping_per_layer_still_works") { auto sim = make_sim(); const std::vector monos{{0}, {1}, {2}}; sim.build_graph(monos, VecZ{0, 1, 2}, VecD{1.0, 1.0, 1.0}, VecZ{0, 0, 1}); // Length graph_layers() -> per-layer branch. sim.set_parameter_mapping(VecZ{0, 0, 0}); - BOOST_TEST(sim.parameter_mapping().size() == 3u); - BOOST_TEST(std::ranges::all_of(sim.parameter_mapping(), [](size_t p) { return p == 0; })); + CHECK(sim.parameter_mapping().size() == 3u); + CHECK(std::ranges::all_of(sim.parameter_mapping(), [](size_t p) { return p == 0; })); } // relabel copies each LayerCore, whose lazily-built derivative exchange layout is eval-time cache, not // data: a mapping set after a gradient must behave exactly like one set before it. -BOOST_AUTO_TEST_CASE(set_parameter_mapping_after_gradient_matches_before) { +TEST_CASE("set_parameter_mapping_after_gradient_matches_before") { const std::vector monos{{0}, {1}, {2}}; const VecD params{0.3, 0.4}; @@ -112,38 +114,38 @@ BOOST_AUTO_TEST_CASE(set_parameter_mapping_after_gradient_matches_before) { after.set_parameter_mapping(VecZ{0, 1}); const auto [value_after, grad_after] = after.expectation_value_and_gradient(params); - BOOST_TEST(value_after == value_before, boost::test_tools::tolerance(1e-12)); - BOOST_REQUIRE_EQUAL(grad_after.size(), grad_before.size()); + CHECK((value_after) == Catch::Approx(value_before).epsilon(1e-12)); + REQUIRE((grad_after.size()) == (grad_before.size())); for (size_t i = 0; i < grad_before.size(); ++i) { - BOOST_TEST(grad_after[i] == grad_before[i], boost::test_tools::tolerance(1e-12)); + CHECK((grad_after[i]) == Catch::Approx(grad_before[i]).epsilon(1e-12)); } } -BOOST_AUTO_TEST_CASE(set_parameter_mapping_rejects_bad_length) { +TEST_CASE("set_parameter_mapping_rejects_bad_length") { auto sim = make_sim(); const std::vector monos{{0}, {1}, {2}}; sim.build_graph(monos, VecZ{0, 1, 2}, VecD{1.0, 1.0, 1.0}, VecZ{0, 0, 1}); // Length 4 matches neither graph_layers (3) nor n_gates (2). - BOOST_CHECK_THROW(sim.set_parameter_mapping(VecZ{0, 1, 2, 3}), std::runtime_error); + CHECK_THROWS_AS(sim.set_parameter_mapping(VecZ{0, 1, 2, 3}), std::runtime_error); } -BOOST_AUTO_TEST_CASE(n_gates_accumulates_across_builds) { +TEST_CASE("n_gates_accumulates_across_builds") { auto sim = make_sim(); sim.build_graph(std::vector{{0}}, VecZ{0}, VecD{1.0}); - BOOST_TEST(sim.n_gates() == 1u); + CHECK(sim.n_gates() == 1u); // Second call's local gate indices (iota) are offset by the existing gate count. sim.build_graph(std::vector{{1}}, VecZ{0}, VecD{1.0}); - BOOST_TEST(sim.n_gates() == 2u); + CHECK(sim.n_gates() == 2u); } -BOOST_AUTO_TEST_CASE(build_graph_rejects_malformed_gate_indices) { +TEST_CASE("build_graph_rejects_malformed_gate_indices") { auto sim = make_sim(); const std::vector monos{{0}, {1}}; // A jump from 0 to 2 is not a contiguous run. - BOOST_CHECK_THROW(sim.build_graph(monos, VecZ{0, 1}, VecD{1.0, 1.0}, VecZ{0, 2}), std::runtime_error); + CHECK_THROWS_AS(sim.build_graph(monos, VecZ{0, 1}, VecD{1.0, 1.0}, VecZ{0, 2}), std::runtime_error); } -BOOST_AUTO_TEST_CASE(coeff_informed_build_graph_rejects_too_few_parameters) { +TEST_CASE("coeff_informed_build_graph_rejects_too_few_parameters") { auto sim = make_sim(); sim.build_graph(std::vector{{0}, {1}}, VecZ{0, 1}, VecD{1.0, 1.0}); @@ -151,15 +153,15 @@ BOOST_AUTO_TEST_CASE(coeff_informed_build_graph_rejects_too_few_parameters) { // (>= 2). Its own mapping references only parameter 0, so a length-1 vector passes the // per-mapping length check but is too short to contract the existing graph -- the guard // rejects it up front rather than seeding from a silently truncated prefix. - BOOST_CHECK_THROW( + CHECK_THROWS_AS( sim.build_graph(std::vector{{2}}, VecZ{0}, VecD{1.0}, std::nullopt, std::optional{VecD{0.5}}), std::invalid_argument); } -BOOST_AUTO_TEST_CASE(contract_partially_replays_existing_graph_and_supports_inplace) { +TEST_CASE("contract_partially_replays_existing_graph_and_supports_inplace") { auto sim = make_sim(); sim.build_graph(std::vector{{0}, {1}}, VecZ{0, 1}, VecD{1.0, 1.0}); - BOOST_TEST(sim.graph_layers() == 2u); + CHECK(sim.graph_layers() == 2u); // Coefficient-informed extend on a non-empty graph: build_graph internally calls // contract_partially(existing_params, /*inplace=*/false) to reseed atol truncation. The new layer's @@ -171,15 +173,15 @@ BOOST_AUTO_TEST_CASE(contract_partially_replays_existing_graph_and_supports_inpl VecD{1.0}, std::nullopt, std::optional{VecD{0.5, 0.25, 0.1}}); - BOOST_TEST(sim.graph_layers() == 3u); + CHECK(sim.graph_layers() == 3u); // inplace=false: returns coefficients, leaves the graph intact. const auto peeked = sim.contract_partially(VecD{0.5, 0.25, 0.1}, false); - BOOST_TEST(!peeked.empty()); - BOOST_TEST(sim.graph_layers() == 3u); + CHECK(!peeked.empty()); + CHECK(sim.graph_layers() == 3u); // inplace=true: consumes the (entire) graph into the operator. const auto consumed = sim.contract_partially(VecD{0.5, 0.25, 0.1}, true); - BOOST_TEST(consumed.size() == peeked.size()); - BOOST_TEST(sim.graph_layers() == 0u); + CHECK(consumed.size() == peeked.size()); + CHECK(sim.graph_layers() == 0u); } diff --git a/cpp/tests/graph_encoding_tests.cpp b/cpp/tests/graph_encoding_tests.cpp index 26bfe2cf..3265ef51 100644 --- a/cpp/tests/graph_encoding_tests.cpp +++ b/cpp/tests/graph_encoding_tests.cpp @@ -15,7 +15,9 @@ // White-box tests for the pure packing/layout functions in // src/monoprop/detail/graph_encoding/*, checked against hand-computed oracles. -#include +#include +#include +#include #include #include @@ -25,7 +27,7 @@ using namespace monoprop; -BOOST_AUTO_TEST_CASE(graph_encoding_word_builder_push_index_coalesces_within_word) { +TEST_CASE("graph_encoding_word_builder_push_index_coalesces_within_word") { CosineWordBuilder b; b.push_index(0); b.push_index(1); @@ -34,135 +36,135 @@ BOOST_AUTO_TEST_CASE(graph_encoding_word_builder_push_index_coalesces_within_wor b.push_index(197); const CosMask cos = b.finish(); - BOOST_REQUIRE_EQUAL(cos.blocks.size(), 3U); - BOOST_CHECK_EQUAL(cos.blocks[0].first, 0U); - BOOST_CHECK_EQUAL(cos.blocks[0].second, 0b1011ULL); - BOOST_CHECK_EQUAL(cos.blocks[1].first, 64U); - BOOST_CHECK_EQUAL(cos.blocks[1].second, 0b1ULL); - BOOST_CHECK_EQUAL(cos.blocks[2].first, 192U); - BOOST_CHECK_EQUAL(cos.blocks[2].second, uint64_t{1} << 5); - BOOST_CHECK_EQUAL(cos.total_count, 5U); + REQUIRE((cos.blocks.size()) == (3U)); + CHECK((cos.blocks[0].first) == (0U)); + CHECK((cos.blocks[0].second) == (0b1011ULL)); + CHECK((cos.blocks[1].first) == (64U)); + CHECK((cos.blocks[1].second) == (0b1ULL)); + CHECK((cos.blocks[2].first) == (192U)); + CHECK((cos.blocks[2].second) == (uint64_t{1} << 5)); + CHECK((cos.total_count) == (5U)); } -BOOST_AUTO_TEST_CASE(graph_encoding_word_builder_push_word_skips_zero_and_counts_bits) { +TEST_CASE("graph_encoding_word_builder_push_word_skips_zero_and_counts_bits") { CosineWordBuilder b; b.push_word(0, 0b101ULL); b.push_word(64, 0ULL); // zero word: no-op, no block emitted b.push_word(128, 0xFULL); const CosMask cos = b.finish(); - BOOST_REQUIRE_EQUAL(cos.blocks.size(), 2U); - BOOST_CHECK_EQUAL(cos.blocks[0].first, 0U); - BOOST_CHECK_EQUAL(cos.blocks[1].first, 128U); - BOOST_CHECK_EQUAL(cos.total_count, 2U + 4U); - BOOST_CHECK_EQUAL(cos.span_count(), 2U); + REQUIRE((cos.blocks.size()) == (2U)); + CHECK((cos.blocks[0].first) == (0U)); + CHECK((cos.blocks[1].first) == (128U)); + CHECK((cos.total_count) == (2U + 4U)); + CHECK((cos.span_count()) == (2U)); } -BOOST_AUTO_TEST_CASE(graph_encoding_word_builder_finish_flushes_pending_and_empty_is_empty) { +TEST_CASE("graph_encoding_word_builder_finish_flushes_pending_and_empty_is_empty") { CosineWordBuilder pending; pending.push_index(5); const CosMask cos = pending.finish(); - BOOST_REQUIRE_EQUAL(cos.blocks.size(), 1U); - BOOST_CHECK_EQUAL(cos.blocks[0].second, uint64_t{1} << 5); + REQUIRE((cos.blocks.size()) == (1U)); + CHECK((cos.blocks[0].second) == (uint64_t{1} << 5)); CosineWordBuilder empty; const CosMask none = empty.finish(); - BOOST_CHECK(none.blocks.empty()); - BOOST_CHECK_EQUAL(none.total_count, 0U); + CHECK(none.blocks.empty()); + CHECK((none.total_count) == (0U)); } -BOOST_AUTO_TEST_CASE(graph_encoding_checked_term_index_boundary) { +TEST_CASE("graph_encoding_checked_term_index_boundary") { // At the TermIndex ceiling it round-trips; above it throws only in the narrow build. const size_t ceiling = static_cast(std::numeric_limits::max()); - BOOST_CHECK_EQUAL(detail::checked_term_index(ceiling, "term"), std::numeric_limits::max()); + CHECK((detail::checked_term_index(ceiling, "term")) == (std::numeric_limits::max())); #if !defined(monoprop_WIDE_TERM_INDEX) - BOOST_CHECK_THROW(detail::checked_term_index(ceiling + 1, "term"), std::overflow_error); + CHECK_THROWS_AS(detail::checked_term_index(ceiling + 1, "term"), std::overflow_error); #else const size_t above_u32 = static_cast(std::numeric_limits::max()) + 1; - BOOST_CHECK_EQUAL(detail::checked_term_index(above_u32, "term"), static_cast(above_u32)); + CHECK((detail::checked_term_index(above_u32, "term")) == (static_cast(above_u32))); #endif } -BOOST_AUTO_TEST_CASE(graph_encoding_checked_packed_phase_bounds) { - BOOST_CHECK_EQUAL(detail::checked_packed_phase(127, "phase"), 127); - BOOST_CHECK_EQUAL(detail::checked_packed_phase(-128, "phase"), -128); - BOOST_CHECK_THROW(detail::checked_packed_phase(128, "phase"), std::overflow_error); - BOOST_CHECK_THROW(detail::checked_packed_phase(-129, "phase"), std::overflow_error); +TEST_CASE("graph_encoding_checked_packed_phase_bounds") { + CHECK((detail::checked_packed_phase(127, "phase")) == (127)); + CHECK((detail::checked_packed_phase(-128, "phase")) == (-128)); + CHECK_THROWS_AS(detail::checked_packed_phase(128, "phase"), std::overflow_error); + CHECK_THROWS_AS(detail::checked_packed_phase(-129, "phase"), std::overflow_error); } -BOOST_AUTO_TEST_CASE(graph_encoding_make_packed_phase_storage_modes_and_zero) { - BOOST_CHECK(detail::make_packed_phase_storage(0, /*binary=*/true).empty()); - BOOST_CHECK(detail::make_packed_phase_storage(0, /*binary=*/false).empty()); +TEST_CASE("graph_encoding_make_packed_phase_storage_modes_and_zero") { + CHECK(detail::make_packed_phase_storage(0, /*binary=*/true).empty()); + CHECK(detail::make_packed_phase_storage(0, /*binary=*/false).empty()); // Binary mode packs 64 phases per word; int8 mode is one byte per phase. const auto binary = detail::make_packed_phase_storage(130, /*binary=*/true); - BOOST_CHECK(binary.uses_binary_phases); - BOOST_CHECK_EQUAL(binary.phase_words.size(), 3U); - BOOST_CHECK(binary.phase_values.empty()); + CHECK(binary.uses_binary_phases); + CHECK((binary.phase_words.size()) == (3U)); + CHECK(binary.phase_values.empty()); const auto wide = detail::make_packed_phase_storage(130, /*binary=*/false); - BOOST_CHECK(!wide.uses_binary_phases); - BOOST_CHECK_EQUAL(wide.phase_values.size(), 130U); - BOOST_CHECK(wide.phase_words.empty()); + CHECK(!wide.uses_binary_phases); + CHECK((wide.phase_values.size()) == (130U)); + CHECK(wide.phase_words.empty()); } -BOOST_AUTO_TEST_CASE(graph_encoding_packed_phase_at_reads_int8_values) { +TEST_CASE("graph_encoding_packed_phase_at_reads_int8_values") { auto storage = detail::make_packed_phase_storage(3, /*binary=*/false); storage.phase_values[0] = 5; storage.phase_values[1] = -7; storage.phase_values[2] = 1; - BOOST_CHECK_EQUAL(detail::packed_phase_at(storage, 0), 5); - BOOST_CHECK_EQUAL(detail::packed_phase_at(storage, 1), -7); - BOOST_CHECK_EQUAL(detail::packed_phase_at(storage, 2), 1); + CHECK((detail::packed_phase_at(storage, 0)) == (5)); + CHECK((detail::packed_phase_at(storage, 1)) == (-7)); + CHECK((detail::packed_phase_at(storage, 2)) == (1)); } -BOOST_AUTO_TEST_CASE(graph_encoding_exchange_layout_scale_and_displacements) { +TEST_CASE("graph_encoding_exchange_layout_scale_and_displacements") { const std::vector send_counts = {3, 0, 5}; const auto s1 = detail::build_layer_exchange_layout(send_counts, /*scale=*/1); - BOOST_CHECK((s1.counts == std::vector{3, 0, 5})); - BOOST_CHECK((s1.displs == std::vector{0, 3, 3})); // prefix sum: 0, 0+3, 3+0 - BOOST_CHECK_EQUAL(s1.total_count, 8U); + CHECK((s1.counts == std::vector{3, 0, 5})); + CHECK((s1.displs == std::vector{0, 3, 3})); // prefix sum: 0, 0+3, 3+0 + CHECK((s1.total_count) == (8U)); const auto s2 = detail::build_layer_exchange_layout(send_counts, /*scale=*/2); - BOOST_CHECK((s2.counts == std::vector{6, 0, 10})); - BOOST_CHECK((s2.displs == std::vector{0, 6, 6})); - BOOST_CHECK_EQUAL(s2.total_count, 16U); + CHECK((s2.counts == std::vector{6, 0, 10})); + CHECK((s2.displs == std::vector{0, 6, 6})); + CHECK((s2.total_count) == (16U)); - BOOST_CHECK_GT(detail::layer_exchange_layout_storage_bytes(s1), 0U); + CHECK((detail::layer_exchange_layout_storage_bytes(s1)) > (0U)); } // Production only builds scale=1; the 2x layout reaches MPI through this accessor, which is // unreachable at comm size 1, so the default non-MPI suite would otherwise never touch it. -BOOST_AUTO_TEST_CASE(graph_encoding_derivative_exchange_layout_is_twice_the_evolution_layout) { +TEST_CASE("graph_encoding_derivative_exchange_layout_is_twice_the_evolution_layout") { LayerCore core; core.evolution_exchange_layout = detail::build_layer_exchange_layout({3, 0, 5}, /*scale=*/1); const auto &derivative = core.derivative_exchange_layout(); - BOOST_CHECK((derivative.counts == std::vector{6, 0, 10})); - BOOST_CHECK((derivative.displs == std::vector{0, 6, 6})); - BOOST_CHECK_EQUAL(derivative.total_count, 16U); + CHECK((derivative.counts == std::vector{6, 0, 10})); + CHECK((derivative.displs == std::vector{0, 6, 6})); + CHECK((derivative.total_count) == (16U)); // Cached: the second read returns the same object, so eval-time MPI holds a stable pointer. - BOOST_CHECK_EQUAL(&core.derivative_exchange_layout(), &derivative); + CHECK((&core.derivative_exchange_layout()) == (&derivative)); // Reset drops the cache (relabel copies cores and must not inherit eval-time state). core.reset_derivative_exchange_layout(); - BOOST_CHECK_EQUAL(core.derivative_exchange_layout().total_count, 16U); + CHECK((core.derivative_exchange_layout().total_count) == (16U)); } -BOOST_AUTO_TEST_CASE(graph_encoding_derivative_exchange_layout_overflow_throws) { +TEST_CASE("graph_encoding_derivative_exchange_layout_overflow_throws") { // A count that fits int at 1x but not at 2x. build_layer_storage_unified runs this derivation // eagerly, so the throw lands in build_graph and not inside the gradient collective window. const size_t just_over_half = static_cast(std::numeric_limits::max()) / 2 + 1; LayerCore core; core.evolution_exchange_layout = detail::build_layer_exchange_layout({just_over_half}, 1); - BOOST_CHECK_THROW(detail::build_derivative_exchange_layout(core.evolution_exchange_layout), std::overflow_error); + CHECK_THROWS_AS(detail::build_derivative_exchange_layout(core.evolution_exchange_layout), std::overflow_error); } -BOOST_AUTO_TEST_CASE(graph_encoding_d_from_b_derivation_both_arms) { +TEST_CASE("graph_encoding_d_from_b_derivation_both_arms") { // B = [in(P=2)] ++ [out(Q=3)] = [10,11 | 20,21,22]; D = [out] ++ [in], derived from B and in_count. std::vector data(1); auto &p = data[0]; @@ -177,13 +179,13 @@ BOOST_AUTO_TEST_CASE(graph_encoding_d_from_b_derivation_both_arms) { const auto storage = detail::build_packed_cross_rank_storage(std::move(data)); // out arm (idx < Q): B[P+idx] - BOOST_CHECK_EQUAL(detail::cross_rank_sin_recv_index(storage, 0, 0), 20U); - BOOST_CHECK_EQUAL(detail::cross_rank_sin_recv_index(storage, 0, 1), 21U); - BOOST_CHECK_EQUAL(detail::cross_rank_sin_recv_index(storage, 0, 2), 22U); + CHECK((detail::cross_rank_sin_recv_index(storage, 0, 0)) == (20U)); + CHECK((detail::cross_rank_sin_recv_index(storage, 0, 1)) == (21U)); + CHECK((detail::cross_rank_sin_recv_index(storage, 0, 2)) == (22U)); // in arm (idx >= Q): B[idx-Q] - BOOST_CHECK_EQUAL(detail::cross_rank_sin_recv_index(storage, 0, 3), 10U); - BOOST_CHECK_EQUAL(detail::cross_rank_sin_recv_index(storage, 0, 4), 11U); + CHECK((detail::cross_rank_sin_recv_index(storage, 0, 3)) == (10U)); + CHECK((detail::cross_rank_sin_recv_index(storage, 0, 4)) == (11U)); // send side reads B verbatim - BOOST_CHECK_EQUAL(detail::cross_rank_sin_send_index(storage, 0, 0), 10U); - BOOST_CHECK_EQUAL(detail::cross_rank_sin_send_index(storage, 0, 4), 22U); + CHECK((detail::cross_rank_sin_send_index(storage, 0, 0)) == (10U)); + CHECK((detail::cross_rank_sin_send_index(storage, 0, 4)) == (22U)); } diff --git a/cpp/tests/hybrid_comm_tests.cpp b/cpp/tests/hybrid_comm_tests.cpp index 6c8db293..63856a0e 100644 --- a/cpp/tests/hybrid_comm_tests.cpp +++ b/cpp/tests/hybrid_comm_tests.cpp @@ -15,7 +15,9 @@ // HybridComm transport equivalence: R MPI ranks x S in-process partitions must behave as one flat P=R*S // SPMD world, with only partition 0 touching MPI, exactly as PartitionGroup drives it. -#include +#include +#include +#include #ifdef monoprop_ENABLE_MPI @@ -56,7 +58,7 @@ auto world_rank() -> int { } // namespace -BOOST_AUTO_TEST_CASE(hybrid_comm_flat_size_and_rank) { +TEST_CASE("hybrid_comm_flat_size_and_rank") { if (world_size() < 2) { return; } @@ -70,17 +72,17 @@ BOOST_AUTO_TEST_CASE(hybrid_comm_flat_size_and_rank) { seen_rank[static_cast(u)] = monoprop::mpi::rank(c); }); for (const auto &e : errs) { - BOOST_CHECK(e == nullptr); + CHECK(e == nullptr); } for (int u = 0; u < S; ++u) { - BOOST_CHECK_EQUAL(seen_size[static_cast(u)], R * S); - BOOST_CHECK_EQUAL(seen_rank[static_cast(u)], world_rank() * S + u); + CHECK((seen_size[static_cast(u)]) == (R * S)); + CHECK((seen_rank[static_cast(u)]) == (world_rank() * S + u)); } } } // Each partition contributes its global id, so the expected total is sum_{g(u)] = monoprop::mpi::allreduce_sum(mine, c); }); for (const auto &e : errs) { - BOOST_CHECK(e == nullptr); + CHECK(e == nullptr); } for (int u = 0; u < S; ++u) { - BOOST_CHECK_EQUAL(got[static_cast(u)], expected); + CHECK((got[static_cast(u)]) == (expected)); } } } // begin_alltoallv must deliver each source's block contiguously in ascending global source order with // tags intact (Resolve.h's positional pairing). -BOOST_AUTO_TEST_CASE(hybrid_comm_alltoallv_source_order_and_tags) { +TEST_CASE("hybrid_comm_alltoallv_source_order_and_tags") { if (world_size() < 2) { return; } @@ -130,17 +132,17 @@ BOOST_AUTO_TEST_CASE(hybrid_comm_alltoallv_source_order_and_tags) { recv[static_cast(u)] = out; }); for (const auto &e : errs) { - BOOST_CHECK(e == nullptr); + CHECK(e == nullptr); } for (int u = 0; u < S; ++u) { const auto &out = recv[static_cast(u)]; - BOOST_REQUIRE_EQUAL(static_cast(out.size()), P); + REQUIRE((static_cast(out.size())) == (P)); for (int src = 0; src < P; ++src) { const int len = src % 3 + 1; const auto &blk = out[static_cast(src)]; - BOOST_REQUIRE_EQUAL(static_cast(blk.size()), len); + REQUIRE((static_cast(blk.size())) == (len)); for (int j = 0; j < len; ++j) { - BOOST_CHECK_EQUAL(blk[static_cast(j)], src * 1000 + j); + CHECK((blk[static_cast(j)]) == (src * 1000 + j)); } } } @@ -149,7 +151,7 @@ BOOST_AUTO_TEST_CASE(hybrid_comm_alltoallv_source_order_and_tags) { // Back-to-back alltoallvs with varying counts (zeros, growth, shrink) on one HybridComm: staging and // offset-table reuse (a stale staged byte surfaces as a wrong tag), and the no-trailing-barrier rule. -BOOST_AUTO_TEST_CASE(hybrid_comm_repeated_alltoallv_varying_sizes) { +TEST_CASE("hybrid_comm_repeated_alltoallv_varying_sizes") { if (world_size() < 2) { return; } @@ -196,14 +198,14 @@ BOOST_AUTO_TEST_CASE(hybrid_comm_repeated_alltoallv_varying_sizes) { } }); for (const auto &e : errs) { - BOOST_CHECK(e == nullptr); + CHECK(e == nullptr); } - BOOST_CHECK_EQUAL(failures.load(), 0); + CHECK((failures.load()) == (0)); } // alltoallv_resolve driven directly: it folds the count MPI_Alltoall into the payload verb's B1→B2 // window and sizes recv itself. -BOOST_AUTO_TEST_CASE(hybrid_comm_alltoallv_resolve_fused) { +TEST_CASE("hybrid_comm_alltoallv_resolve_fused") { if (world_size() < 2) { return; } @@ -260,12 +262,12 @@ BOOST_AUTO_TEST_CASE(hybrid_comm_alltoallv_resolve_fused) { } }); for (const auto &e : errs) { - BOOST_CHECK(e == nullptr); + CHECK(e == nullptr); } - BOOST_CHECK_EQUAL(failures.load(), 0); + CHECK((failures.load()) == (0)); } -BOOST_AUTO_TEST_CASE(hybrid_comm_allreduce_sum_inplace_global) { +TEST_CASE("hybrid_comm_allreduce_sum_inplace_global") { if (world_size() < 2) { return; } @@ -286,16 +288,16 @@ BOOST_AUTO_TEST_CASE(hybrid_comm_allreduce_sum_inplace_global) { res[static_cast(u)] = v; }); for (const auto &e : errs) { - BOOST_CHECK(e == nullptr); + CHECK(e == nullptr); } // sum over g of ((g+1)*0.25 + k) = P(P+1)/8 + P*k for (int u = 0; u < S; ++u) { - BOOST_REQUIRE_EQUAL(res[static_cast(u)].size(), N); + REQUIRE((res[static_cast(u)].size()) == (N)); for (size_t k = 0; k < N; ++k) { const double expect = static_cast(P) * (P + 1) / 8.0 + static_cast(P) * static_cast(k); - BOOST_CHECK_CLOSE(res[static_cast(u)][k], expect, 1e-12); - BOOST_CHECK_EQUAL(res[static_cast(u)][k], res[0][k]); // bit-identical + CHECK((res[static_cast(u)][k]) == Catch::Approx(expect).epsilon((1e-12) / 100.0)); + CHECK((res[static_cast(u)][k]) == (res[0][k])); // bit-identical } } } @@ -305,7 +307,7 @@ BOOST_AUTO_TEST_CASE(hybrid_comm_allreduce_sum_inplace_global) { // Poison releases barrier waiters on every rank; the test completing at all proves that. Partition 0 poisons // before entering a collective, so no rank is committed to MPI and the partition-0 guard deliberately does // not fire. Poisoning inside a collective calls MPI_Abort instead, so it cannot be a ctest case. -BOOST_AUTO_TEST_CASE(hybrid_comm_poison_releases_waiters) { +TEST_CASE("hybrid_comm_poison_releases_waiters") { if (world_size() < 2) { return; } @@ -319,10 +321,10 @@ BOOST_AUTO_TEST_CASE(hybrid_comm_poison_releases_waiters) { std::vector got(static_cast(world_size() * S)); hyb.alltoall_counts(u, send.data(), got.data()); }); - BOOST_CHECK(errs[0] == nullptr); + CHECK(errs[0] == nullptr); for (int u = 1; u < S; ++u) { - BOOST_REQUIRE(errs[static_cast(u)] != nullptr); - BOOST_CHECK_THROW(std::rethrow_exception(errs[static_cast(u)]), monoprop::mpi::ShmCommPoisoned); + REQUIRE(errs[static_cast(u)] != nullptr); + CHECK_THROWS_AS(std::rethrow_exception(errs[static_cast(u)]), monoprop::mpi::ShmCommPoisoned); } } } diff --git a/cpp/tests/inverted_index_tests.cpp b/cpp/tests/inverted_index_tests.cpp index a5819c7e..72cdeb74 100644 --- a/cpp/tests/inverted_index_tests.cpp +++ b/cpp/tests/inverted_index_tests.cpp @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include +#include +#include +#include #include #include @@ -62,7 +64,7 @@ auto rows_of(const Sc &sc, size_t c) -> std::vector { } } // namespace -BOOST_AUTO_TEST_CASE(inverted_index_row_parity_matches_popcount) { +TEST_CASE("inverted_index_row_parity_matches_popcount") { const std::vector op{ bs({0, 1}), bs({0, 1, 2}), @@ -71,7 +73,7 @@ BOOST_AUTO_TEST_CASE(inverted_index_row_parity_matches_popcount) { }; Sc sc; sc.rebuild(op); - BOOST_TEST(sc.rows() == op.size()); + CHECK(sc.rows() == op.size()); const uint64_t *parity = sc.row_parity_words(); bool all_match = true; @@ -81,13 +83,13 @@ BOOST_AUTO_TEST_CASE(inverted_index_row_parity_matches_popcount) { all_match = false; } } - BOOST_TEST(all_match); + CHECK(all_match); // row_parity_words is idempotent (a lazy cache): a second call must not change the bitmap. - BOOST_TEST(((sc.row_parity_words()[0] >> 1U) & 1U) == 1U); // row 1 is odd + CHECK(((sc.row_parity_words()[0] >> 1U) & 1U) == 1U); // row 1 is odd } // rebuild decides tiers from the final per-column counts: dense once density >= 1/kPromoteDensityInv. -BOOST_AUTO_TEST_CASE(inverted_index_promotes_column_at_density_crossover) { +TEST_CASE("inverted_index_promotes_column_at_density_crossover") { constexpr size_t kR = 128; // threshold = ceil(128/64) = 2 set rows to go dense std::vector op; op.reserve(kR); @@ -106,16 +108,16 @@ BOOST_AUTO_TEST_CASE(inverted_index_promotes_column_at_density_crossover) { } Sc sc; sc.rebuild(op); - BOOST_TEST(sc.rows() == kR); - BOOST_TEST(sc.column_is_dense(col_of(0))); // 10/128 >= 1/64 - BOOST_TEST(!sc.column_is_dense(col_of(1))); // 1/128 < 1/64 - BOOST_TEST(sc.sparse_column_rows(col_of(1)).size() == 1u); - BOOST_TEST(sc.sparse_column_rows(col_of(1))[0] == 0u); + CHECK(sc.rows() == kR); + CHECK(sc.column_is_dense(col_of(0))); // 10/128 >= 1/64 + CHECK(!sc.column_is_dense(col_of(1))); // 1/128 < 1/64 + CHECK(sc.sparse_column_rows(col_of(1)).size() == 1u); + CHECK(sc.sparse_column_rows(col_of(1))[0] == 0u); } // rebuild fills columns in row order, so sparse row-lists come out ascending — the invariant // combine_columns_block's lower_bound relies on. -BOOST_AUTO_TEST_CASE(inverted_index_fill_yields_ascending_sparse_rows) { +TEST_CASE("inverted_index_fill_yields_ascending_sparse_rows") { constexpr size_t M = 64; // 2M = 128 columns using ScW = InvertedIndex; constexpr size_t kR = 16'385; // large operator, many sparse columns @@ -127,7 +129,7 @@ BOOST_AUTO_TEST_CASE(inverted_index_fill_yields_ascending_sparse_rows) { } ScW sc; sc.rebuild(op); - BOOST_TEST(sc.rows() == kR); + CHECK(sc.rows() == kR); bool all_sorted = true; bool saw_nonempty_sparse = false; @@ -143,15 +145,15 @@ BOOST_AUTO_TEST_CASE(inverted_index_fill_yields_ascending_sparse_rows) { all_sorted = false; } } - BOOST_TEST(saw_nonempty_sparse); // the fill actually populated sparse columns - BOOST_TEST(all_sorted); + CHECK(saw_nonempty_sparse); // the fill actually populated sparse columns + CHECK(all_sorted); } // append_rows is the incremental growth path (MPOperator appends as terms are inserted); rebuild is the // from-scratch one. On the same final row set the two must agree on membership. Tier choice legitimately // differs -- rebuild sees the final per-column counts up front, while the append path promotes on // crossing the threshold -- so the comparison is on set rows, which are tier-independent. -BOOST_AUTO_TEST_CASE(inverted_index_append_rows_matches_rebuild) { +TEST_CASE("inverted_index_append_rows_matches_rebuild") { constexpr size_t kR = 200; std::vector op; op.reserve(kR); @@ -177,10 +179,11 @@ BOOST_AUTO_TEST_CASE(inverted_index_append_rows_matches_rebuild) { inc.append_rows(op, 64, 100); inc.append_rows(op, 164, kR - 164); - BOOST_REQUIRE_EQUAL(inc.rows(), full.rows()); + REQUIRE((inc.rows()) == (full.rows())); for (size_t c = 0; c < Sc::kNumColumns; ++c) { - BOOST_TEST_CONTEXT("column " << c) { - BOOST_TEST(rows_of(inc, c) == rows_of(full, c), boost::test_tools::per_element()); + { + INFO("column " << c); + CHECK(rows_of(inc, c) == rows_of(full, c)); } } @@ -193,14 +196,14 @@ BOOST_AUTO_TEST_CASE(inverted_index_append_rows_matches_rebuild) { parity_matches = false; } } - BOOST_TEST(parity_matches); + CHECK(parity_matches); } // combine_columns_block is the fold-combine kernel every scan and recompute path shares. It XORs the // given columns' row bitmaps over a word range: XOR associativity means any block decomposition // reproduces the full-width fold bit-for-bit, and the dense-column memcpy seed must equal // memset + XOR-all. -BOOST_AUTO_TEST_CASE(combine_columns_block_folds_dense_and_sparse_identically) { +TEST_CASE("combine_columns_block_folds_dense_and_sparse_identically") { constexpr size_t kR = 300; // 5 row words, so a block split has something to split std::vector op; std::vector in_a(kR, false), in_b(kR, false), in_sparse(kR, false); @@ -226,9 +229,9 @@ BOOST_AUTO_TEST_CASE(combine_columns_block_folds_dense_and_sparse_identically) { } Sc sc; sc.rebuild(op); - BOOST_REQUIRE(sc.column_is_dense(col_of(0))); - BOOST_REQUIRE(sc.column_is_dense(col_of(1))); - BOOST_REQUIRE(!sc.column_is_dense(col_of(2))); + REQUIRE(sc.column_is_dense(col_of(0))); + REQUIRE(sc.column_is_dense(col_of(1))); + REQUIRE(!sc.column_is_dense(col_of(2))); const size_t words = (kR + 63) / 64; const std::vector cols{col_of(0), col_of(1), col_of(2)}; @@ -243,13 +246,13 @@ BOOST_AUTO_TEST_CASE(combine_columns_block_folds_dense_and_sparse_identically) { std::vector whole(words, 0xdeadbeefULL); // pre-dirtied: the kernel seeds, never accumulates combine_columns_block(sc, cols, whole.data(), 0, words); - BOOST_TEST(whole == expected, boost::test_tools::per_element()); + CHECK(whole == expected); std::vector pieced(words, 0xdeadbeefULL); for (size_t w = 0; w < words; ++w) { combine_columns_block(sc, cols, pieced.data() + w, w, w + 1); } - BOOST_TEST(pieced == expected, boost::test_tools::per_element()); + CHECK(pieced == expected); // Sparse-only column list: no dense column to memcpy from, so this is the memset seed path. const size_t sparse_col = col_of(2); @@ -258,5 +261,5 @@ BOOST_AUTO_TEST_CASE(combine_columns_block_folds_dense_and_sparse_identically) { std::vector sparse_expected(words, 0); sparse_expected[5 >> 6] |= uint64_t{1} << (5 & 63U); sparse_expected[200 >> 6] |= uint64_t{1} << (200 & 63U); - BOOST_TEST(sparse_fold == sparse_expected, boost::test_tools::per_element()); + CHECK(sparse_fold == sparse_expected); } diff --git a/cpp/tests/large_cosine_storage_tests.cpp b/cpp/tests/large_cosine_storage_tests.cpp index 2110b355..ccac2eb3 100644 --- a/cpp/tests/large_cosine_storage_tests.cpp +++ b/cpp/tests/large_cosine_storage_tests.cpp @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include +#include +#include +#include #include #include @@ -23,7 +25,7 @@ using namespace monoprop; -BOOST_AUTO_TEST_CASE(pruned_layer_supports_cos_counts_above_u32) { +TEST_CASE("pruned_layer_supports_cos_counts_above_u32") { const size_t large_count = static_cast(std::numeric_limits::max()) + 9; const size_t large_index = static_cast(std::numeric_limits::max()) + 17; @@ -57,12 +59,12 @@ BOOST_AUTO_TEST_CASE(pruned_layer_supports_cos_counts_above_u32) { Layer layer{storage, std::move(pruned_cos)}; const auto lt = layer.traversal(); - BOOST_CHECK_EQUAL(lt.num_cos_inds(), large_count); + CHECK((lt.num_cos_inds()) == (large_count)); // Cross-rank is read verbatim from the core (never masked): B[0] = in-block[0] = 200. size_t b_idx = static_cast(-1); lt.for_each_cross_rank_sin_send_range(1, 0, 1, [&](size_t, size_t i) { b_idx = i; }); - BOOST_CHECK_EQUAL(b_idx, 200UL); + CHECK((b_idx) == (200UL)); // D[0] is derived from B: Q = sin_recv_count - in_count = 20 - 12 = 8, so D[0] = out-block[0] = 100, // stored phase = -(out_phases[0]) = -(+1) = -1. @@ -72,23 +74,23 @@ BOOST_AUTO_TEST_CASE(pruned_layer_supports_cos_counts_above_u32) { d_idx = i; d_phi = phi; }); - BOOST_CHECK_EQUAL(d_idx, 100UL); - BOOST_CHECK_EQUAL(d_phi, -1); + CHECK((d_idx) == (100UL)); + CHECK((d_phi) == (-1)); } // The per-rank cross-rank counts index into one layer's term set, so under the wide build they must // be TermIndex-wide; uint32_t would silently cap a single partition/layer at ~2^32 terms. -BOOST_AUTO_TEST_CASE(cross_rank_partner_range_counts_track_term_index_width) { +TEST_CASE("cross_rank_partner_range_counts_track_term_index_width") { CrossRankPartnerRange r{}; - BOOST_CHECK_EQUAL(sizeof(r.sin_send_count), sizeof(TermIndex)); - BOOST_CHECK_EQUAL(sizeof(r.sin_recv_count), sizeof(TermIndex)); - BOOST_CHECK_EQUAL(sizeof(r.in_count), sizeof(TermIndex)); + CHECK((sizeof(r.sin_send_count)) == (sizeof(TermIndex))); + CHECK((sizeof(r.sin_recv_count)) == (sizeof(TermIndex))); + CHECK((sizeof(r.in_count)) == (sizeof(TermIndex))); } #if defined(monoprop_WIDE_TERM_INDEX) // Under the wide build (TermIndex = u64), a cross-rank B (partner term) index above 2^32 must // round-trip losslessly through the packed cross-rank storage rather than hit a UINT32_MAX cap. -BOOST_AUTO_TEST_CASE(cross_rank_sin_send_index_round_trips_above_u32) { +TEST_CASE("cross_rank_sin_send_index_round_trips_above_u32") { const size_t big_in = static_cast(std::numeric_limits::max()) + 1000; const size_t big_out = static_cast(std::numeric_limits::max()) + 5; @@ -103,22 +105,22 @@ BOOST_AUTO_TEST_CASE(cross_rank_sin_send_index_round_trips_above_u32) { const auto storage = detail::build_packed_cross_rank_storage(std::move(cross_rank)); - BOOST_CHECK_EQUAL(detail::cross_rank_sin_send_index(storage, 1, 0), big_in); - BOOST_CHECK_EQUAL(detail::cross_rank_sin_send_index(storage, 1, 1), big_out); + CHECK((detail::cross_rank_sin_send_index(storage, 1, 0)) == (big_in)); + CHECK((detail::cross_rank_sin_send_index(storage, 1, 1)) == (big_out)); // D[0] is derived from B: Q = sin_recv_count - in_count = 1, so D[0] = out-block[0] = big_out. - BOOST_CHECK_EQUAL(detail::cross_rank_sin_recv_index(storage, 1, 0), big_out); + CHECK((detail::cross_rank_sin_recv_index(storage, 1, 0)) == (big_out)); } #endif // The cross-rank exchange uses MPI int counts/displacements, so a single per-rank exchange is capped // at INT_MAX elements; checked_mpi_int must throw cleanly at that limit, never wrap silently. -BOOST_AUTO_TEST_CASE(checked_mpi_int_throws_cleanly_above_int_max) { +TEST_CASE("checked_mpi_int_throws_cleanly_above_int_max") { const size_t at_limit = static_cast(std::numeric_limits::max()); - BOOST_CHECK_EQUAL(detail::checked_mpi_int(at_limit, "exchange count"), std::numeric_limits::max()); - BOOST_CHECK_THROW(detail::checked_mpi_int(at_limit + 1, "exchange count"), std::overflow_error); + CHECK((detail::checked_mpi_int(at_limit, "exchange count")) == (std::numeric_limits::max())); + CHECK_THROWS_AS(detail::checked_mpi_int(at_limit + 1, "exchange count"), std::overflow_error); } -BOOST_AUTO_TEST_CASE(cosine_word_list_scale_and_accumulate) { +TEST_CASE("cosine_word_list_scale_and_accumulate") { using monoprop::CosMask; CosMask cos; cos.blocks = {{0, 0b1011ULL}, {64, 0b1ULL}, {192, (1ULL << 5)}}; @@ -127,24 +129,24 @@ BOOST_AUTO_TEST_CASE(cosine_word_list_scale_and_accumulate) { const std::vector base_coeff(256, 2.0); std::vector par = base_coeff; monoprop::detail::scale_cos_mask(par.data(), cos, 3.0); - BOOST_TEST(par[0] == 6.0); - BOOST_TEST(par[1] == 6.0); - BOOST_TEST(par[3] == 6.0); - BOOST_TEST(par[2] == 2.0); - BOOST_TEST(par[64] == 6.0); - BOOST_TEST(par[197] == 6.0); - BOOST_TEST(par[100] == 2.0); + CHECK(par[0] == 6.0); + CHECK(par[1] == 6.0); + CHECK(par[3] == 6.0); + CHECK(par[2] == 2.0); + CHECK(par[64] == 6.0); + CHECK(par[197] == 6.0); + CHECK(par[100] == 2.0); std::vector pp(256, 1.5), ph(256, 0.5); const double a_par = monoprop::detail::accumulate_cos_mask(pp.data(), ph.data(), cos, 0.7, 0.9); // Returns sum(state[i]*ham[i]) over the 5 set indices, taken before the scaling below. - BOOST_TEST(a_par == 5.0 * 1.5 * 0.5, boost::test_tools::tolerance(1e-12)); + CHECK((a_par) == Catch::Approx(5.0 * 1.5 * 0.5).epsilon(1e-12)); // state and ham at set indices scaled by cos_val and sec_val respectively - BOOST_TEST(pp[0] == 1.5 * 0.7, boost::test_tools::tolerance(1e-12)); - BOOST_TEST(ph[0] == 0.5 * 0.9, boost::test_tools::tolerance(1e-12)); + CHECK((pp[0]) == Catch::Approx(1.5 * 0.7).epsilon(1e-12)); + CHECK((ph[0]) == Catch::Approx(0.5 * 0.9).epsilon(1e-12)); } -BOOST_AUTO_TEST_CASE(packed_cross_rank_storage_bit_packs_binary_phases) { +TEST_CASE("packed_cross_rank_storage_bit_packs_binary_phases") { std::vector binary_cross_rank(2); std::vector wide_phase_cross_rank(2); @@ -174,14 +176,13 @@ BOOST_AUTO_TEST_CASE(packed_cross_rank_storage_bit_packs_binary_phases) { const auto wide_phase_storage = detail::build_packed_cross_rank_storage(std::move(wide_phase_cross_rank)); // All input phases are ±1 so the binary storage uses 1-bit packing. - BOOST_CHECK(binary_storage.sin_recv_phases.uses_binary_phases); - BOOST_CHECK(!wide_phase_storage.sin_recv_phases.uses_binary_phases); + CHECK(binary_storage.sin_recv_phases.uses_binary_phases); + CHECK(!wide_phase_storage.sin_recv_phases.uses_binary_phases); // D^-[1] = term idx+5=6, phase -1, stored negated: -(-1) = 1. - BOOST_CHECK_EQUAL(detail::cross_rank_sin_recv_phase(binary_storage, 1, 1), 1); + CHECK((detail::cross_rank_sin_recv_phase(binary_storage, 1, 1)) == (1)); // D^+[0] = term idx+1005=1005, phase +1, stored as-is. (D^+ starts at flat index 128.) - BOOST_CHECK_EQUAL(detail::cross_rank_sin_recv_phase(binary_storage, 1, 128), 1); + CHECK((detail::cross_rank_sin_recv_phase(binary_storage, 1, 128)) == (1)); - BOOST_CHECK_LT(detail::cross_rank_storage_bytes(binary_storage), - detail::cross_rank_storage_bytes(wide_phase_storage)); + CHECK((detail::cross_rank_storage_bytes(binary_storage)) < (detail::cross_rank_storage_bytes(wide_phase_storage))); } diff --git a/cpp/tests/majorana_cutoff_tests.cpp b/cpp/tests/majorana_cutoff_tests.cpp index bea9a5bd..f1211514 100644 --- a/cpp/tests/majorana_cutoff_tests.cpp +++ b/cpp/tests/majorana_cutoff_tests.cpp @@ -15,7 +15,9 @@ // Sets are built directly in raw-bit space (Monomial::set) so the "fully paired" condition is // unambiguous: a pair is raw bits (2k, 2k+1). -#include +#include +#include +#include #include #include @@ -28,33 +30,33 @@ using namespace monoprop; using cd = std::complex; // Raw bits {0,1} and {4,5} are two complete pairs. -BOOST_AUTO_TEST_CASE(majorana_cutoff_paired_kept_unconditionally) { +TEST_CASE("majorana_cutoff_paired_kept_unconditionally") { constexpr size_t N = 32; Monomial paired; paired.set(0); paired.set(1); paired.set(4); paired.set(5); - BOOST_TEST(is_paired(paired)); - BOOST_TEST(length_cutoff(paired, 0)); - BOOST_TEST(length_cutoff(paired, 2)); - BOOST_TEST(support_cutoff(paired, 0)); + CHECK(is_paired(paired)); + CHECK(length_cutoff(paired, 0)); + CHECK(length_cutoff(paired, 2)); + CHECK(support_cutoff(paired, 0)); } // An unpaired set of length 3 (raw bits {0,2,4}: each even bit lacks its odd partner). -BOOST_AUTO_TEST_CASE(majorana_cutoff_length_and_support_thresholds) { +TEST_CASE("majorana_cutoff_length_and_support_thresholds") { constexpr size_t N = 32; Monomial unpaired; unpaired.set(0); unpaired.set(2); unpaired.set(4); - BOOST_TEST(!is_paired(unpaired)); + CHECK(!is_paired(unpaired)); // length = popcount = 3; support (distinct orbitals) = 3 here. - BOOST_TEST(length_cutoff(unpaired, 3)); - BOOST_TEST(!length_cutoff(unpaired, 2)); - BOOST_TEST(support_cutoff(unpaired, 3)); - BOOST_TEST(!support_cutoff(unpaired, 2)); + CHECK(length_cutoff(unpaired, 3)); + CHECK(!length_cutoff(unpaired, 2)); + CHECK(support_cutoff(unpaired, 3)); + CHECK(!support_cutoff(unpaired, 2)); // support <= length always, so passing length implies passing support at the same cutoff. std::mt19937_64 rng(0x50FA11ULL); @@ -66,71 +68,71 @@ BOOST_AUTO_TEST_CASE(majorana_cutoff_length_and_support_thresholds) { } for (unsigned int c : {0U, 1U, 2U, 3U}) { if (length_cutoff(m, c)) { - BOOST_TEST(support_cutoff(m, c)); + CHECK(support_cutoff(m, c)); } } - BOOST_TEST(length_cutoff(m, 2 * N)); - BOOST_TEST(length_cutoff(m, 0) == is_paired(m)); + CHECK(length_cutoff(m, 2 * N)); + CHECK(length_cutoff(m, 0) == is_paired(m)); } } // logical_num_modes masks off the inactive low-mode prefix, so bits there must not count against the // active window. -BOOST_AUTO_TEST_CASE(majorana_cutoff_logical_num_modes_masks_prefix_single_word) { +TEST_CASE("majorana_cutoff_logical_num_modes_masks_prefix_single_word") { constexpr size_t N = 32; constexpr size_t logical = 6; // active window = raw bits [2*(32-6), 64) = [52, 64) Monomial prefix_only; prefix_only.set(0); // lone unpaired bit, inside the inactive prefix // Active window is empty -> treated as fully paired -> kept even at cutoff 0. - BOOST_TEST(length_cutoff(prefix_only, 0, logical)); - BOOST_TEST(support_cutoff(prefix_only, 0, logical)); + CHECK(length_cutoff(prefix_only, 0, logical)); + CHECK(support_cutoff(prefix_only, 0, logical)); // Over the whole register the lone bit is unpaired and exceeds cutoff 0 -> dropped. - BOOST_TEST(!length_cutoff(prefix_only, 0, N)); - BOOST_TEST(!length_cutoff(prefix_only, 0)); // whole-register overload + CHECK(!length_cutoff(prefix_only, 0, N)); + CHECK(!length_cutoff(prefix_only, 0)); // whole-register overload Monomial active_bit; active_bit.set(52); - BOOST_TEST(!length_cutoff(active_bit, 0, logical)); + CHECK(!length_cutoff(active_bit, 0, logical)); Monomial active_pair; active_pair.set(52); active_pair.set(53); - BOOST_TEST(length_cutoff(active_pair, 0, logical)); + CHECK(length_cutoff(active_pair, 0, logical)); } -BOOST_AUTO_TEST_CASE(majorana_cutoff_logical_num_modes_masks_prefix_multi_word) { +TEST_CASE("majorana_cutoff_logical_num_modes_masks_prefix_multi_word") { constexpr size_t N = 96; constexpr size_t logical = 90; // active window = raw bits [2*(96-90), 192) = [12, 192) Monomial prefix_only; prefix_only.set(4); // lone unpaired bit in the inactive prefix - BOOST_TEST(length_cutoff(prefix_only, 0, logical)); // active window empty -> kept - BOOST_TEST(!length_cutoff(prefix_only, 0, N)); // whole register -> dropped + CHECK(length_cutoff(prefix_only, 0, logical)); // active window empty -> kept + CHECK(!length_cutoff(prefix_only, 0, N)); // whole register -> dropped } -BOOST_AUTO_TEST_CASE(majorana_cutoff_evaluator_dispatch_and_popcount) { +TEST_CASE("majorana_cutoff_evaluator_dispatch_and_popcount") { constexpr size_t N = 32; CutoffFn length_fn = detail::LengthCutoff{.cutoff = 3}; detail::CutoffEvaluator length_ev(length_fn); - BOOST_TEST((length_ev.length_cutoff() != nullptr)); - BOOST_TEST((length_ev.support_cutoff() == nullptr)); - BOOST_REQUIRE(length_ev.max_slot_bound().has_value()); + CHECK((length_ev.length_cutoff() != nullptr)); + CHECK((length_ev.support_cutoff() == nullptr)); + REQUIRE(length_ev.max_slot_bound().has_value()); // A length cutoff counts set bits directly, so the slot bound IS the cutoff. - BOOST_TEST(length_ev.max_slot_bound().value() == 3U); + CHECK(length_ev.max_slot_bound().value() == 3U); CutoffFn support_fn = detail::SupportCutoff{.cutoff = 2}; detail::CutoffEvaluator support_ev(support_fn); - BOOST_TEST((support_ev.length_cutoff() == nullptr)); - BOOST_TEST((support_ev.support_cutoff() != nullptr)); + CHECK((support_ev.length_cutoff() == nullptr)); + CHECK((support_ev.support_cutoff() != nullptr)); // A support cutoff counts modes/qubits and each spans two slots, so the slot bound doubles. - BOOST_TEST(support_ev.max_slot_bound().value() == 4U); + CHECK(support_ev.max_slot_bound().value() == 4U); CutoffFn opaque_fn = [](const Monomial &) { return true; }; detail::CutoffEvaluator opaque_ev(opaque_fn); - BOOST_TEST((opaque_ev.length_cutoff() == nullptr)); - BOOST_TEST((opaque_ev.support_cutoff() == nullptr)); - BOOST_TEST(!opaque_ev.max_slot_bound().has_value()); + CHECK((opaque_ev.length_cutoff() == nullptr)); + CHECK((opaque_ev.support_cutoff() == nullptr)); + CHECK(!opaque_ev.max_slot_bound().has_value()); // passes_with_popcount: pc <= cutoff short-circuits to true; otherwise it equals a direct eval. Monomial unpaired; // length 4, not paired @@ -138,20 +140,20 @@ BOOST_AUTO_TEST_CASE(majorana_cutoff_evaluator_dispatch_and_popcount) { unpaired.set(2); unpaired.set(4); unpaired.set(6); - BOOST_TEST(length_ev.passes_with_popcount(unpaired, 3)); - BOOST_TEST(!length_ev.passes_with_popcount(unpaired, 4)); - BOOST_TEST(length_ev.passes_with_popcount(unpaired, 4) == length_ev(unpaired)); + CHECK(length_ev.passes_with_popcount(unpaired, 3)); + CHECK(!length_ev.passes_with_popcount(unpaired, 4)); + CHECK(length_ev.passes_with_popcount(unpaired, 4) == length_ev(unpaired)); Monomial paired; // pc>cutoff but paired -> direct eval keeps it paired.set(0); paired.set(1); paired.set(2); paired.set(3); - BOOST_TEST(length_ev.passes_with_popcount(paired, 10)); + CHECK(length_ev.passes_with_popcount(paired, 10)); } // interleave_phase (reference prefix-XOR scan) must equal the masked-parity form used on the hot path. -BOOST_AUTO_TEST_CASE(majorana_cutoff_interleave_phase_mask_cross_check) { +TEST_CASE("majorana_cutoff_interleave_phase_mask_cross_check") { auto check = [](auto tag) { constexpr size_t N = decltype(tag)::value; std::mt19937_64 rng(0xABCDEF01ULL + N); @@ -166,14 +168,14 @@ BOOST_AUTO_TEST_CASE(majorana_cutoff_interleave_phase_mask_cross_check) { const int reference = interleave_phase(m, g); const auto w = interleave_phase_mask(g); const int masked = m.parity_and(w) ? -1 : 1; - BOOST_TEST(reference == masked); + CHECK(reference == masked); } }; check(std::integral_constant{}); // single word check(std::integral_constant{}); // multi word } -BOOST_AUTO_TEST_CASE(majorana_cutoff_encode_decode_coeff) { +TEST_CASE("majorana_cutoff_encode_decode_coeff") { constexpr size_t N = 32; Monomial mono; mono.set(0); @@ -182,29 +184,29 @@ BOOST_AUTO_TEST_CASE(majorana_cutoff_encode_decode_coeff) { for (double r : {1.0, -2.5, 0.0, 7.25}) { const cd hermitian = decode_coeff(cd(r, 0.0), mono); // r * hermitian_coefficient(mono) - BOOST_TEST(encode_coeff(hermitian, mono) == r); + CHECK(encode_coeff(hermitian, mono) == r); } // Multiply by i to break Hermiticity: the encoded value then has a nonzero imaginary part. const cd non_hermitian = decode_coeff(cd(1.0, 0.0), mono) * cd(0.0, 1.0); - BOOST_CHECK_THROW(encode_coeff(non_hermitian, mono), std::runtime_error); + CHECK_THROWS_AS(encode_coeff(non_hermitian, mono), std::runtime_error); } // max_ones counts pairs, so it saturates at logical_num_modes, not at the bit count 2*logical_num_modes. // Over-asking must land on the same full set rather than indexing past the pair selector. -BOOST_AUTO_TEST_CASE(majorana_cutoff_paired_op_saturates_at_one_pair_per_mode) { +TEST_CASE("majorana_cutoff_paired_op_saturates_at_one_pair_per_mode") { constexpr size_t N = 32; constexpr size_t kLogical = 4; const auto full = generate_paired_op(kLogical, kLogical); // Every subset of the kLogical pairs, so 2^kLogical monomials. - BOOST_TEST(full.size() == (size_t{1} << kLogical)); + CHECK(full.size() == (size_t{1} << kLogical)); for (const size_t over : {kLogical + 1, 2 * kLogical, 2 * kLogical + 3}) { const auto clamped = generate_paired_op(over, kLogical); - BOOST_TEST(clamped.size() == full.size()); + CHECK(clamped.size() == full.size()); for (size_t i = 0; i < full.size(); ++i) { - BOOST_TEST(clamped[i] == full[i]); + CHECK(clamped[i] == full[i]); } } } diff --git a/cpp/tests/mp_graph_tests.cpp b/cpp/tests/mp_graph_tests.cpp index 2f7e6bb2..9ca5e0d1 100644 --- a/cpp/tests/mp_graph_tests.cpp +++ b/cpp/tests/mp_graph_tests.cpp @@ -15,7 +15,9 @@ // White-box tests for MPGraph transforms and MPGraphView, built by direct Layer construction // (GraphBuildHarness). Each layer's distinct gate_index is the oracle for slice / view ordering. -#include +#include +#include +#include #include #include @@ -28,95 +30,95 @@ using test_utils::core_with_gate; using test_utils::graph_with_gates; using test_utils::layer_with_gate; -BOOST_AUTO_TEST_CASE(mp_graph_slice_graph_heisenberg_prefix_no_contract) { +TEST_CASE("mp_graph_slice_graph_heisenberg_prefix_no_contract") { auto graph = graph_with_gates(/*schrodinger=*/false, 5); // layers_ = [0,1,2,3,4] auto sliced = graph.slice_graph(3, /*contract=*/false); - BOOST_REQUIRE_EQUAL(sliced.layers(), 3U); - BOOST_CHECK_EQUAL(sliced.get_layer_traversal(0).gate_index(), 0U); - BOOST_CHECK_EQUAL(sliced.get_layer_traversal(1).gate_index(), 1U); - BOOST_CHECK_EQUAL(sliced.get_layer_traversal(2).gate_index(), 2U); + REQUIRE((sliced.layers()) == (3U)); + CHECK((sliced.get_layer_traversal(0).gate_index()) == (0U)); + CHECK((sliced.get_layer_traversal(1).gate_index()) == (1U)); + CHECK((sliced.get_layer_traversal(2).gate_index()) == (2U)); // Non-contracting slice leaves the source untouched. - BOOST_CHECK_EQUAL(graph.layers(), 5U); - BOOST_CHECK_EQUAL(graph.get_layer_traversal(0).gate_index(), 0U); + CHECK((graph.layers()) == (5U)); + CHECK((graph.get_layer_traversal(0).gate_index()) == (0U)); } -BOOST_AUTO_TEST_CASE(mp_graph_slice_graph_schrodinger_contract_newest_first_copy_and_resize) { +TEST_CASE("mp_graph_slice_graph_schrodinger_contract_newest_first_copy_and_resize") { // Schrödinger stores newest-first: appending gates 0..4 gives layers_ = [4,3,2,1,0]. auto graph = graph_with_gates(/*schrodinger=*/true, 5); auto sliced = graph.slice_graph(2, /*contract=*/true); // sliced = layers_[active_end-1-i] = layers_[4], layers_[3] = gates 0, 1 (oldest-first). - BOOST_REQUIRE_EQUAL(sliced.layers(), 2U); - BOOST_CHECK_EQUAL(sliced.get_layer_traversal(0).gate_index(), 0U); - BOOST_CHECK_EQUAL(sliced.get_layer_traversal(1).gate_index(), 1U); + REQUIRE((sliced.layers()) == (2U)); + CHECK((sliced.get_layer_traversal(0).gate_index()) == (0U)); + CHECK((sliced.get_layer_traversal(1).gate_index()) == (1U)); // Contract resized layers_ to the newest 3 (gates 4,3,2, still newest-first). - BOOST_REQUIRE_EQUAL(graph.layers(), 3U); - BOOST_CHECK_EQUAL(graph.get_layer_traversal(0).gate_index(), 4U); - BOOST_CHECK_EQUAL(graph.get_layer_traversal(1).gate_index(), 3U); - BOOST_CHECK_EQUAL(graph.get_layer_traversal(2).gate_index(), 2U); + REQUIRE((graph.layers()) == (3U)); + CHECK((graph.get_layer_traversal(0).gate_index()) == (4U)); + CHECK((graph.get_layer_traversal(1).gate_index()) == (3U)); + CHECK((graph.get_layer_traversal(2).gate_index()) == (2U)); } -BOOST_AUTO_TEST_CASE(mp_graph_slice_graph_key_clamped_to_size) { +TEST_CASE("mp_graph_slice_graph_key_clamped_to_size") { auto graph = graph_with_gates(/*schrodinger=*/false, 3); auto sliced = graph.slice_graph(100, /*contract=*/false); - BOOST_CHECK_EQUAL(sliced.layers(), 3U); + CHECK((sliced.layers()) == (3U)); } // The maybe_compact_layers arms below are reached through Heisenberg slice_graph(contract=true). -BOOST_AUTO_TEST_CASE(mp_graph_contract_clear_arm_when_prefix_covers_all) { +TEST_CASE("mp_graph_contract_clear_arm_when_prefix_covers_all") { auto graph = graph_with_gates(/*schrodinger=*/false, 5); (void)graph.slice_graph(5, /*contract=*/true); // front_offset == size -> clear - BOOST_CHECK_EQUAL(graph.layers(), 0U); + CHECK((graph.layers()) == (0U)); // Graph is still usable after a full clear. graph.append(std::make_shared(), 0, 0.0, /*gate_index=*/42); - BOOST_REQUIRE_EQUAL(graph.layers(), 1U); - BOOST_CHECK_EQUAL(graph.get_layer_traversal(0).gate_index(), 42U); + REQUIRE((graph.layers()) == (1U)); + CHECK((graph.get_layer_traversal(0).gate_index()) == (42U)); } -BOOST_AUTO_TEST_CASE(mp_graph_contract_noop_arm_keeps_dead_prefix_lazy) { +TEST_CASE("mp_graph_contract_noop_arm_keeps_dead_prefix_lazy") { auto graph = graph_with_gates(/*schrodinger=*/false, 100); (void)graph.slice_graph(3, /*contract=*/true); // front_offset 3 < 4096 -> no physical compaction - BOOST_REQUIRE_EQUAL(graph.layers(), 97U); - BOOST_CHECK_EQUAL(graph.get_layer_traversal(0).gate_index(), 3U); - BOOST_CHECK_EQUAL(graph.get_layer_traversal(96).gate_index(), 99U); + REQUIRE((graph.layers()) == (97U)); + CHECK((graph.get_layer_traversal(0).gate_index()) == (3U)); + CHECK((graph.get_layer_traversal(96).gate_index()) == (99U)); } -BOOST_AUTO_TEST_CASE(mp_graph_contract_erase_arm_above_threshold) { +TEST_CASE("mp_graph_contract_erase_arm_above_threshold") { // The erase arm fires only when front_offset >= 4096 AND 2*front_offset >= size. auto graph = graph_with_gates(/*schrodinger=*/false, 8200); auto sliced = graph.slice_graph(4100, /*contract=*/true); - BOOST_CHECK_EQUAL(sliced.layers(), 4100U); - BOOST_CHECK_EQUAL(sliced.get_layer_traversal(0).gate_index(), 0U); + CHECK((sliced.layers()) == (4100U)); + CHECK((sliced.get_layer_traversal(0).gate_index()) == (0U)); - BOOST_REQUIRE_EQUAL(graph.layers(), 4100U); - BOOST_CHECK_EQUAL(graph.get_layer_traversal(0).gate_index(), 4100U); - BOOST_CHECK_EQUAL(graph.get_layer_traversal(4099).gate_index(), 8199U); + REQUIRE((graph.layers()) == (4100U)); + CHECK((graph.get_layer_traversal(0).gate_index()) == (4100U)); + CHECK((graph.get_layer_traversal(4099).gate_index()) == (8199U)); } -BOOST_AUTO_TEST_CASE(mp_graph_slice_view_heisenberg_forward_window) { +TEST_CASE("mp_graph_slice_view_heisenberg_forward_window") { auto graph = graph_with_gates(/*schrodinger=*/false, 5); auto view = graph.slice_view(3); - BOOST_REQUIRE_EQUAL(view.layers(), 3U); - BOOST_CHECK_EQUAL(view.get_layer_traversal(0).gate_index(), 0U); - BOOST_CHECK_EQUAL(view.get_layer_traversal(1).gate_index(), 1U); - BOOST_CHECK_EQUAL(view.get_layer_traversal(2).gate_index(), 2U); + REQUIRE((view.layers()) == (3U)); + CHECK((view.get_layer_traversal(0).gate_index()) == (0U)); + CHECK((view.get_layer_traversal(1).gate_index()) == (1U)); + CHECK((view.get_layer_traversal(2).gate_index()) == (2U)); } -BOOST_AUTO_TEST_CASE(mp_graph_slice_view_schrodinger_reversed_window) { +TEST_CASE("mp_graph_slice_view_schrodinger_reversed_window") { // layers_ = [4,3,2,1,0]; slice_view(3) uses base=active_end-3=2, reverse=true. // get_layer_traversal(i) -> layers_[2 + (3-1-i)] -> gates 0,1,2 in replay order. auto graph = graph_with_gates(/*schrodinger=*/true, 5); auto view = graph.slice_view(3); - BOOST_REQUIRE_EQUAL(view.layers(), 3U); - BOOST_CHECK_EQUAL(view.get_layer_traversal(0).gate_index(), 0U); - BOOST_CHECK_EQUAL(view.get_layer_traversal(1).gate_index(), 1U); - BOOST_CHECK_EQUAL(view.get_layer_traversal(2).gate_index(), 2U); + REQUIRE((view.layers()) == (3U)); + CHECK((view.get_layer_traversal(0).gate_index()) == (0U)); + CHECK((view.get_layer_traversal(1).gate_index()) == (1U)); + CHECK((view.get_layer_traversal(2).gate_index()) == (2U)); } -BOOST_AUTO_TEST_CASE(mp_graph_view_reverse_flag_flips_index_mapping) { +TEST_CASE("mp_graph_view_reverse_flag_flips_index_mapping") { std::vector layers; for (std::size_t g = 10; g < 14; ++g) { layers.push_back(layer_with_gate(g)); // [10,11,12,13] @@ -125,18 +127,18 @@ BOOST_AUTO_TEST_CASE(mp_graph_view_reverse_flag_flips_index_mapping) { const MPGraphView fwd(layers, /*base=*/0, /*count=*/4, /*reverse=*/false); const MPGraphView rev(layers, /*base=*/0, /*count=*/4, /*reverse=*/true); for (std::size_t i = 0; i < 4; ++i) { - BOOST_CHECK_EQUAL(fwd.get_layer_traversal(i).gate_index(), 10U + i); - BOOST_CHECK_EQUAL(rev.get_layer_traversal(i).gate_index(), 13U - i); + CHECK((fwd.get_layer_traversal(i).gate_index()) == (10U + i)); + CHECK((rev.get_layer_traversal(i).gate_index()) == (13U - i)); } - BOOST_CHECK_THROW(fwd.get_layer(4), std::out_of_range); - BOOST_CHECK_THROW(rev.get_layer(4), std::out_of_range); + CHECK_THROWS_AS(fwd.get_layer(4), std::out_of_range); + CHECK_THROWS_AS(rev.get_layer(4), std::out_of_range); } -BOOST_AUTO_TEST_CASE(mp_graph_get_layer_out_of_range_throws) { +TEST_CASE("mp_graph_get_layer_out_of_range_throws") { auto graph = graph_with_gates(/*schrodinger=*/false, 3); - BOOST_CHECK_NO_THROW((void)graph.get_layer(2)); - BOOST_CHECK_THROW((void)graph.get_layer(3), std::out_of_range); + CHECK_NOTHROW((void)graph.get_layer(2)); + CHECK_THROWS_AS((void)graph.get_layer(3), std::out_of_range); // const overload takes the same guard. const auto &cref = graph; - BOOST_CHECK_THROW((void)cref.get_layer(3), std::out_of_range); + CHECK_THROWS_AS((void)cref.get_layer(3), std::out_of_range); } diff --git a/cpp/tests/mp_operator_tests.cpp b/cpp/tests/mp_operator_tests.cpp index d0a4698d..c40b13de 100644 --- a/cpp/tests/mp_operator_tests.cpp +++ b/cpp/tests/mp_operator_tests.cpp @@ -17,7 +17,9 @@ // encode_pauli_coeff) as the oracle. They pin composition -- incremental scoring, slot placement, the // init-map drain, the picture/basis branches -- not the phase math (majorana_cutoff_tests.cpp). -#include +#include +#include +#include #include #include @@ -79,7 +81,7 @@ auto sparse_state_equals(const detail::MPOperator<8>::SparseState &sparse, const } // namespace -BOOST_AUTO_TEST_CASE(mp_operator_get_state_scores_paired_terms_majorana_and_pauli) { +TEST_CASE("mp_operator_get_state_scores_paired_terms_majorana_and_pauli") { const VecZ initial_state = {0, 1}; // occupied modes for (const Basis basis : {Basis::Majorana, Basis::Pauli}) { detail::MPOperator<8> op; @@ -99,24 +101,24 @@ BOOST_AUTO_TEST_CASE(mp_operator_get_state_scores_paired_terms_majorana_and_paul // The sparse form is the resting representation: paired rows only, ascending. const auto sparse = op.sparse_state(); - BOOST_CHECK(sparse_state_equals(sparse, expected_sparse_state(op, basis, initial_state))); - BOOST_REQUIRE_EQUAL(sparse.rows.size(), 2U); // rows 0 and 1; the unpaired row 2 is absent - BOOST_CHECK_EQUAL(sparse.rows[0], 0U); - BOOST_CHECK_EQUAL(sparse.rows[1], 1U); + CHECK(sparse_state_equals(sparse, expected_sparse_state(op, basis, initial_state))); + REQUIRE((sparse.rows.size()) == (2U)); // rows 0 and 1; the unpaired row 2 is absent + CHECK((sparse.rows[0]) == (0U)); + CHECK((sparse.rows[1]) == (1U)); const VecD state = op.materialize_state(); - BOOST_REQUIRE_EQUAL(state.size(), 3U); - BOOST_CHECK(state == expected_state(op, basis, initial_state)); - BOOST_CHECK(op.dense_state() == state); + REQUIRE((state.size()) == (3U)); + CHECK(state == expected_state(op, basis, initial_state)); + CHECK(op.dense_state() == state); // Structural, oracle-independent: paired rows carry a unit phase, the unpaired row is zero. - BOOST_CHECK_EQUAL(std::abs(state[0]), 1.0); - BOOST_CHECK_EQUAL(std::abs(state[1]), 1.0); - BOOST_CHECK_EQUAL(state[2], 0.0); + CHECK((std::abs(state[0])) == (1.0)); + CHECK((std::abs(state[1])) == (1.0)); + CHECK((state[2]) == (0.0)); } } -BOOST_AUTO_TEST_CASE(mp_operator_get_state_scores_only_new_terms_incrementally) { +TEST_CASE("mp_operator_get_state_scores_only_new_terms_incrementally") { const VecZ initial_state = {0}; detail::MPOperator<8> op; op.initial_state = initial_state; @@ -126,9 +128,9 @@ BOOST_AUTO_TEST_CASE(mp_operator_get_state_scores_only_new_terms_incrementally) a.set(1); // paired op.append_term(a); const VecD first = op.dense_state(); - BOOST_REQUIRE_EQUAL(first.size(), 1U); + REQUIRE((first.size()) == (1U)); const double a_score = first[0]; - BOOST_CHECK_EQUAL(op.state_scored_rows_, 1U); + CHECK((op.state_scored_rows_) == (1U)); // Stands in for evolution mutating the live vector: the incremental pass must not rewrite an // already-scored row, so this value has to survive. @@ -139,21 +141,21 @@ BOOST_AUTO_TEST_CASE(mp_operator_get_state_scores_only_new_terms_incrementally) b.set(3); // paired op.append_term(b); const VecD second = op.dense_state(); // must score only row 1, leave row 0 untouched - BOOST_REQUIRE_EQUAL(second.size(), 2U); - BOOST_CHECK_EQUAL(second[0], 7.5); - BOOST_CHECK_EQUAL(second[1], expected_state(op, Basis::Majorana, initial_state)[1]); + REQUIRE((second.size()) == (2U)); + CHECK((second[0]) == (7.5)); + CHECK((second[1]) == (expected_state(op, Basis::Majorana, initial_state)[1])); // The sparse set was extended, not rebuilt: row 0 still carries its original state score. const auto sparse = op.sparse_state(); - BOOST_CHECK(sparse_state_equals(sparse, expected_sparse_state(op, Basis::Majorana, initial_state))); - BOOST_REQUIRE_EQUAL(sparse.rows.size(), 2U); - BOOST_CHECK_EQUAL(sparse.values[0], a_score); + CHECK(sparse_state_equals(sparse, expected_sparse_state(op, Basis::Majorana, initial_state))); + REQUIRE((sparse.rows.size()) == (2U)); + CHECK((sparse.values[0]) == (a_score)); // Idempotent when nothing was appended. - BOOST_CHECK(op.dense_state() == second); + CHECK(op.dense_state() == second); } -BOOST_AUTO_TEST_CASE(mp_operator_get_operator_drains_present_terms_from_init_map) { +TEST_CASE("mp_operator_get_operator_drains_present_terms_from_init_map") { const auto a = indices_to_bitset<8>({0, 1}); const auto b = indices_to_bitset<8>({2, 3}); auto op = build_indexed_op({a, b}); @@ -163,17 +165,17 @@ BOOST_AUTO_TEST_CASE(mp_operator_get_operator_drains_present_terms_from_init_map op.init_op_map[absent] = 9.0; // absent from store -> stays pending const VecD &coeffs = op.get_operator(); - BOOST_REQUIRE_EQUAL(coeffs.size(), 2U); - BOOST_CHECK_EQUAL(coeffs[0], 3.0); - BOOST_CHECK_EQUAL(coeffs[1], 0.0); // b was not in the init map - BOOST_CHECK(op.init_op_map.find(a) == op.init_op_map.end()); // drained - BOOST_CHECK(op.init_op_map.find(absent) != op.init_op_map.end()); // retained + REQUIRE((coeffs.size()) == (2U)); + CHECK((coeffs[0]) == (3.0)); + CHECK((coeffs[1]) == (0.0)); // b was not in the init map + CHECK(op.init_op_map.find(a) == op.init_op_map.end()); // drained + CHECK(op.init_op_map.find(absent) != op.init_op_map.end()); // retained // Second call is a no-op fast path (size already matches). - BOOST_CHECK(op.get_operator() == coeffs); + CHECK(op.get_operator() == coeffs); } -BOOST_AUTO_TEST_CASE(mp_operator_update_initial_operator_heisenberg_branches_pauli) { +TEST_CASE("mp_operator_update_initial_operator_heisenberg_branches_pauli") { const auto present = indices_to_bitset<8>({0, 2}); auto op = build_indexed_op({present}, Basis::Pauli); // row 0 indexed op.init_op_map[indices_to_bitset<8>({4, 6})] = 0.0; // seed a pending term @@ -183,32 +185,32 @@ BOOST_AUTO_TEST_CASE(mp_operator_update_initial_operator_heisenberg_branches_pau dict[VecZ{4, 6}] = cd(2.5, 0.0); // in init_op_map -> stays pending const auto grad = op.update_initial_operator(dict, /*schrodinger=*/false); - BOOST_REQUIRE_EQUAL(op.op_coeffs.size(), 1U); - BOOST_CHECK_EQUAL(op.op_coeffs[0], encode_pauli_coeff(cd(1.5, 0.0))); // Pauli encode path - BOOST_CHECK(op.init_op_map.find(indices_to_bitset<8>({4, 6})) != op.init_op_map.end()); - BOOST_CHECK(op.init_op_map.find(present) == op.init_op_map.end()); - BOOST_CHECK_EQUAL(grad.first.size(), 2U); // every supplied term recorded in the grad arrays + REQUIRE((op.op_coeffs.size()) == (1U)); + CHECK((op.op_coeffs[0]) == (encode_pauli_coeff(cd(1.5, 0.0)))); // Pauli encode path + CHECK(op.init_op_map.find(indices_to_bitset<8>({4, 6})) != op.init_op_map.end()); + CHECK(op.init_op_map.find(present) == op.init_op_map.end()); + CHECK((grad.first.size()) == (2U)); // every supplied term recorded in the grad arrays } -BOOST_AUTO_TEST_CASE(mp_operator_update_initial_operator_heisenberg_rejects_absent_term) { +TEST_CASE("mp_operator_update_initial_operator_heisenberg_rejects_absent_term") { auto op = build_indexed_op({indices_to_bitset<8>({0, 2})}, Basis::Pauli); OperatorDict dict; dict[VecZ{1, 3, 5}] = cd(1.0, 0.0); // absent from both store and init_op_map - BOOST_CHECK_THROW(op.update_initial_operator(dict, /*schrodinger=*/false), std::runtime_error); + CHECK_THROWS_AS(op.update_initial_operator(dict, /*schrodinger=*/false), std::runtime_error); } -BOOST_AUTO_TEST_CASE(mp_operator_update_initial_operator_schrodinger_admits_absent_term) { +TEST_CASE("mp_operator_update_initial_operator_schrodinger_admits_absent_term") { auto op = build_indexed_op({indices_to_bitset<8>({0, 2})}, Basis::Pauli); OperatorDict dict; const auto fresh = indices_to_bitset<8>({1, 3, 5}); dict[VecZ{1, 3, 5}] = cd(4.0, 0.0); op.update_initial_operator(dict, /*schrodinger=*/true); - BOOST_CHECK(op.init_op_map.find(fresh) != op.init_op_map.end()); + CHECK(op.init_op_map.find(fresh) != op.init_op_map.end()); } -BOOST_AUTO_TEST_CASE(mp_operator_update_initial_operator_majorana_encode_identity_term) { +TEST_CASE("mp_operator_update_initial_operator_majorana_encode_identity_term") { // The Majorana codec divides by the term's hermitian phase, which is 1 for the identity term, so // a real coefficient round-trips as itself without tripping the non-Hermitian guard. const Monomial<8> identity; // empty @@ -217,12 +219,12 @@ BOOST_AUTO_TEST_CASE(mp_operator_update_initial_operator_majorana_encode_identit OperatorDict dict; dict[VecZ{}] = cd(2.75, 0.0); op.update_initial_operator(dict, /*schrodinger=*/false); - BOOST_REQUIRE_EQUAL(op.op_coeffs.size(), 1U); - BOOST_CHECK_EQUAL(op.op_coeffs[0], algebra_encode_coeff<8>(Basis::Majorana, cd(2.75, 0.0), identity)); - BOOST_CHECK_EQUAL(op.op_coeffs[0], 2.75); + REQUIRE((op.op_coeffs.size()) == (1U)); + CHECK((op.op_coeffs[0]) == (algebra_encode_coeff<8>(Basis::Majorana, cd(2.75, 0.0), identity))); + CHECK((op.op_coeffs[0]) == (2.75)); } -BOOST_AUTO_TEST_CASE(mp_operator_insert_absent_terms_grows_and_indexes) { +TEST_CASE("mp_operator_insert_absent_terms_grows_and_indexes") { const auto e0 = indices_to_bitset<8>({0, 1}); const auto e1 = indices_to_bitset<8>({2, 3}); auto op = build_indexed_op({e0, e1}); @@ -237,55 +239,55 @@ BOOST_AUTO_TEST_CASE(mp_operator_insert_absent_terms_grows_and_indexes) { [&](size_t k) -> const Monomial<8> & { return fresh[k]; }, [&](size_t k, size_t b) { assign_row<8>(*op.store, b + k, fresh[k]); }); - BOOST_CHECK_EQUAL(base, 2U); - BOOST_CHECK_EQUAL(op.size(), 5U); + CHECK((base) == (2U)); + CHECK((op.size()) == (5U)); for (const auto &f : fresh) { - BOOST_CHECK(op.store->find(f).has_value()); + CHECK(op.store->find(f).has_value()); } - BOOST_CHECK(op.store->find(e0).has_value()); // existing rows intact - BOOST_CHECK(op.store->find(e1).has_value()); + CHECK(op.store->find(e0).has_value()); // existing rows intact + CHECK(op.store->find(e1).has_value()); } -BOOST_AUTO_TEST_CASE(mp_operator_append_term_after_materialization_rebuilds_inverted_index) { +TEST_CASE("mp_operator_append_term_after_materialization_rebuilds_inverted_index") { detail::MPOperator<8> op; op.append_term(indices_to_bitset<8>({0, 1})); - BOOST_CHECK_EQUAL(op.inverted_index().rows(), 1U); // materializes the index (rows == size) + CHECK((op.inverted_index().rows()) == (1U)); // materializes the index (rows == size) // append_term does not sync the index; the next inverted_index() sees rows() != store size and // rebuilds against the grown store. op.append_term(indices_to_bitset<8>({2, 3})); - BOOST_CHECK_EQUAL(op.inverted_index().rows(), 2U); + CHECK((op.inverted_index().rows()) == (2U)); } -BOOST_AUTO_TEST_CASE(mp_operator_estimate_memory_usage_tracks_inverted_index_presence) { +TEST_CASE("mp_operator_estimate_memory_usage_tracks_inverted_index_presence") { detail::MPOperator<8> op; op.append_term(indices_to_bitset<8>({0, 1})); op.append_term(indices_to_bitset<8>({2, 3})); const auto before = detail::estimate_memory_usage<8>(op); - BOOST_CHECK_GT(before.total_bytes(), 0U); - BOOST_CHECK_GT(before.operator_terms_bytes, 0U); - BOOST_CHECK_EQUAL(before.inverted_index_bytes, 0U); // absent arm + CHECK((before.total_bytes()) > (0U)); + CHECK((before.operator_terms_bytes) > (0U)); + CHECK((before.inverted_index_bytes) == (0U)); // absent arm (void)op.inverted_index(); const auto after = detail::estimate_memory_usage<8>(op); - BOOST_CHECK_GT(after.inverted_index_bytes, 0U); // present arm + CHECK((after.inverted_index_bytes) > (0U)); // present arm } -BOOST_AUTO_TEST_CASE(mp_operator_copy_constructor_clones_store_and_coeffs) { +TEST_CASE("mp_operator_copy_constructor_clones_store_and_coeffs") { auto op = build_indexed_op({indices_to_bitset<8>({0, 1}), indices_to_bitset<8>({2, 3})}); op.initial_state = {0}; (void)op.sparse_state(); detail::MPOperator<8> copy(op); // deep copy via clone() - BOOST_CHECK_EQUAL(copy.size(), op.size()); - BOOST_CHECK_EQUAL(copy.state_scored_rows_, op.state_scored_rows_); - BOOST_CHECK(copy.state_rows_ == op.state_rows_); - BOOST_CHECK(copy.state_vals_ == op.state_vals_); - BOOST_CHECK(copy.materialize_state() == op.materialize_state()); - BOOST_CHECK(copy.store->find(indices_to_bitset<8>({0, 1})).has_value()); + CHECK((copy.size()) == (op.size())); + CHECK((copy.state_scored_rows_) == (op.state_scored_rows_)); + CHECK(copy.state_rows_ == op.state_rows_); + CHECK(copy.state_vals_ == op.state_vals_); + CHECK(copy.materialize_state() == op.materialize_state()); + CHECK(copy.store->find(indices_to_bitset<8>({0, 1})).has_value()); // Mutating the copy must not touch the original (independent stores). copy.append_term(indices_to_bitset<8>({4, 5})); - BOOST_CHECK_EQUAL(op.size(), 2U); - BOOST_CHECK_EQUAL(copy.size(), 3U); + CHECK((op.size()) == (2U)); + CHECK((copy.size()) == (3U)); } diff --git a/cpp/tests/mpfunctions.cpp b/cpp/tests/mpfunctions.cpp index b4ac2727..01085e9d 100644 --- a/cpp/tests/mpfunctions.cpp +++ b/cpp/tests/mpfunctions.cpp @@ -12,9 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include -#include -#include +#include +#include +#include +#include +#include #include #include @@ -27,9 +29,6 @@ using namespace monoprop; -namespace utf = boost::unit_test; -namespace bdata = utf::data; - constexpr int NumQubits = 4; static std::vector ds_input_indices_to_bitset_test = { {0, 1, 2, 3}, // Full indices set @@ -46,12 +45,13 @@ static std::vector> ds_output_indices_to_bitset_test = { }; -BOOST_DATA_TEST_CASE(indices_to_bitset_test, - bdata::make(ds_input_indices_to_bitset_test) ^ ds_output_indices_to_bitset_test, - input_indices, - expected_bitset) { +TEST_CASE("indices_to_bitset_test") { + const auto index = GENERATE(0U, 1U, 2U, 3U); + const auto& input_indices = ds_input_indices_to_bitset_test[index]; + const auto& expected_bitset = ds_output_indices_to_bitset_test[index]; + CAPTURE(index); auto bitset = indices_to_bitset(input_indices); - BOOST_CHECK(bitset == expected_bitset); + CHECK(bitset == expected_bitset); } static std::vector> ds_input_bitset_to_indices_test = { @@ -70,35 +70,35 @@ static std::vector ds_expected_length_results = { false // length_cutoff keeps iff fully paired or slot count <= cutoff }; -BOOST_DATA_TEST_CASE(length_cutoff_test, - bdata::make(ds_input_bitset_to_indices_test) ^ bdata::make(ds_cutoff_values) - ^ ds_expected_length_results, - input_bitset, - cutoff, - expected_result) { +TEST_CASE("length_cutoff_test") { + const auto index = GENERATE(0U, 1U, 2U); + const auto& input_bitset = ds_input_bitset_to_indices_test[index]; + const auto cutoff = ds_cutoff_values[index]; + const auto expected_result = ds_expected_length_results[index]; + CAPTURE(index); auto result = length_cutoff(input_bitset, cutoff); - BOOST_CHECK(result == expected_result); + CHECK(result == expected_result); } -BOOST_AUTO_TEST_CASE(test_fermionic_to_binary_operator_empty) { +TEST_CASE("test_fermionic_to_binary_operator_empty") { std::vector empty_operator; auto result = fermionic_to_binary_operator(empty_operator); - BOOST_CHECK(result.empty()); + CHECK(result.empty()); } -BOOST_AUTO_TEST_CASE(test_fermionic_to_binary_operator_single_term) { +TEST_CASE("test_fermionic_to_binary_operator_single_term") { std::vector single_term_operator = {{0, 1, 2}}; auto result = fermionic_to_binary_operator(single_term_operator); - BOOST_CHECK(result.size() == 1); - BOOST_CHECK(result[0] == 0b11100000); + CHECK(result.size() == 1); + CHECK(result[0] == 0b11100000); } -BOOST_AUTO_TEST_CASE(test_fermionic_to_binary_operator_multiple_terms) { +TEST_CASE("test_fermionic_to_binary_operator_multiple_terms") { std::vector multi_term_operator = {{0, 1}, {2, 3}}; auto result = fermionic_to_binary_operator(multi_term_operator); - BOOST_CHECK(result.size() == 2); - BOOST_CHECK(result[0] == 0b11000000); - BOOST_CHECK(result[1] == 0b00110000); + CHECK(result.size() == 2); + CHECK(result[0] == 0b11000000); + CHECK(result[1] == 0b00110000); } constexpr size_t NumQubits2 = 2; @@ -106,7 +106,8 @@ static std::vector, int>> ds_get_multiplicative_p {{0b0101}, -1}, {{0b1001}, 1}}; -BOOST_DATA_TEST_CASE(get_multiplicative_phase_test, bdata::make(ds_get_multiplicative_phase), test_pair) { +TEST_CASE("get_multiplicative_phase_test") { + const auto& test_pair = GENERATE_REF(Catch::Generators::from_range(ds_get_multiplicative_phase)); auto [majorana_set, expected_phase] = test_pair; VecZ gen_vec = {0, 1}; auto gen_bitset = indices_to_bitset(gen_vec); @@ -114,7 +115,7 @@ BOOST_DATA_TEST_CASE(get_multiplicative_phase_test, bdata::make(ds_get_multiplic auto gen_count = gen_bitset.count(); auto overlap = (majorana_set & gen_bitset).count(); auto result = get_multiplicative_phase(majorana_set, gen_bitset, mono_count, gen_count, overlap); - BOOST_CHECK(result == expected_phase); + CHECK(result == expected_phase); } struct IS_FULLY_PAIRED_TEST_CASE { @@ -133,20 +134,22 @@ static std::vector ds_is_fully_paired_test = { {{0, 1, 2, 3}, {0b0000, 0b0011, 0b1100, 0b1111}, {0, 1, 2, 3}, "Everything is paired"}, {{0, 1, 2, 3, 4, 5, 6}, {0b0001, 0b0011, 0b1000, 0b0101, 0b1100, 0b0110, 0b1110}, {1, 4}, "Partially paired"}}; -BOOST_DATA_TEST_CASE(is_fully_paired_test, bdata::make(ds_is_fully_paired_test), test_case) { +TEST_CASE("is_fully_paired_test") { + const auto& test_case = GENERATE_REF(Catch::Generators::from_range(ds_is_fully_paired_test)); + CAPTURE(test_case.test_name); auto result = is_fully_paired(test_case.inds, test_case.op_terms); - BOOST_CHECK(std::is_permutation(result.cbegin(), result.cend(), test_case.expected_result.cbegin())); + CHECK(std::is_permutation(result.cbegin(), result.cend(), test_case.expected_result.cbegin())); } -BOOST_AUTO_TEST_CASE(bit_flipping_utilities) { +TEST_CASE("bit_flipping_utilities") { auto val1 = even_bits<10, LSb0>(); auto val2 = odd_bits<10, LSb0>(); auto val3 = even_bits<10, MSb0>(); auto val4 = odd_bits<10, MSb0>(); - BOOST_TEST(val1 == 0b0101010101); - BOOST_TEST(val2 == 0b1010101010); - BOOST_TEST(val3 == 0b1010101010); - BOOST_TEST(val4 == 0b0101010101); + CHECK(val1 == 0b0101010101); + CHECK(val2 == 0b1010101010); + CHECK(val3 == 0b1010101010); + CHECK(val4 == 0b0101010101); } // The evaluation functional carries the reference state sparsely, so every EvalState operation has to @@ -183,101 +186,103 @@ auto make_op(size_t length) -> VecD { } // namespace // dot() over the sparse rows is bit-identical to the dense inner product, hence CHECK_EQUAL. -BOOST_AUTO_TEST_CASE(eval_state_sparse_dot_is_bit_identical_to_dense) { +TEST_CASE("eval_state_sparse_dot_is_bit_identical_to_dense") { const auto op = make_op(kStateLength); const auto dense = make_dense(kStateLength, kRows, kVals); const auto sparse = EvalState::sparse(kStateLength, kRows, kVals); - BOOST_CHECK_EQUAL(sparse.length(), kStateLength); - BOOST_CHECK_EQUAL(sparse.dot(op), inner_product(dense, op)); - BOOST_CHECK_EQUAL(EvalState::dense(dense).dot(op), inner_product(dense, op)); + CHECK((sparse.length()) == (kStateLength)); + CHECK((sparse.dot(op)) == (inner_product(dense, op))); + CHECK((EvalState::dense(dense).dot(op)) == (inner_product(dense, op))); // No scored rows at all (an operator with no fully-paired terms) is a legal state, not a shortcut: // the caller still has to reach its allreduce. const auto empty = EvalState::sparse(kStateLength, {}, {}); - BOOST_CHECK_EQUAL(empty.length(), kStateLength); - BOOST_CHECK_EQUAL(empty.dot(op), 0.0); + CHECK((empty.length()) == (kStateLength)); + CHECK((empty.dot(op)) == (0.0)); // An operator longer than the state is fine (dot spans the state); shorter is a hard error. VecD longer = op; longer.push_back(1.0); - BOOST_CHECK_EQUAL(sparse.dot(longer), sparse.dot(op)); + CHECK((sparse.dot(longer)) == (sparse.dot(op))); const VecD shorter(kStateLength - 1, 1.0); - BOOST_CHECK_THROW(sparse.dot(shorter), std::invalid_argument); + CHECK_THROWS_AS(sparse.dot(shorter), std::invalid_argument); } // scatter_into() must assign: its only caller hands it thread-local scratch holding a previous, longer // state, which resize-and-scatter would leak through. -BOOST_AUTO_TEST_CASE(eval_state_scatter_into_overwrites_a_dirty_buffer) { +TEST_CASE("eval_state_scatter_into_overwrites_a_dirty_buffer") { const auto dense = make_dense(kStateLength, kRows, kVals); const auto sparse = EvalState::sparse(kStateLength, kRows, kVals); VecD out; sparse.scatter_into(out); - BOOST_CHECK(out == dense); + CHECK(out == dense); VecD dirty(kStateLength * 2, 7.5); sparse.scatter_into(dirty); - BOOST_CHECK_EQUAL(dirty.size(), kStateLength); - BOOST_CHECK(dirty == dense); + CHECK((dirty.size()) == (kStateLength)); + CHECK(dirty == dense); // Pre-dirtied at exactly the right length: the size check alone must not let the scatter be skipped. VecD same_length(kStateLength, 7.5); sparse.scatter_into(same_length); - BOOST_CHECK(same_length == dense); + CHECK(same_length == dense); sparse.scatter_into(same_length); - BOOST_CHECK(same_length == dense); + CHECK(same_length == dense); VecD from_dense(3, -1.0); EvalState::dense(dense).scatter_into(from_dense); - BOOST_CHECK(from_dense == dense); + CHECK(from_dense == dense); } // indices_above() is the paring keep-set. It must match the dense scan for every threshold -- including // a negative one, where |0.0| > threshold keeps even the unscored rows. -BOOST_AUTO_TEST_CASE(eval_state_indices_above_matches_the_dense_scan) { +TEST_CASE("eval_state_indices_above_matches_the_dense_scan") { const auto dense = make_dense(kStateLength, kRows, kVals); const auto sparse = EvalState::sparse(kStateLength, kRows, kVals); const std::vector thresholds = {-1.0, -0.0, 0.0, 1e-12, 0.5, std::nextafter(1.0, 0.0), 1.0, 2.0, std::numeric_limits::quiet_NaN()}; for (const auto t : thresholds) { - BOOST_TEST_CONTEXT("threshold = " << t) { + { + INFO("threshold = " << t); const auto expected = indices_above(dense, t); - BOOST_CHECK(sparse.indices_above(t) == expected); - BOOST_CHECK(EvalState::dense(dense).indices_above(t) == expected); + CHECK(sparse.indices_above(t) == expected); + CHECK(EvalState::dense(dense).indices_above(t) == expected); } } // Spot-check the two ends rather than trusting the dense oracle alone. - BOOST_CHECK_EQUAL(sparse.indices_above(0.0).size(), kRows.size()); - BOOST_CHECK_EQUAL(sparse.indices_above(-1.0).size(), kStateLength); - BOOST_CHECK(sparse.indices_above(1.0).empty()); - BOOST_CHECK(sparse.indices_above(std::numeric_limits::quiet_NaN()).empty()); + CHECK((sparse.indices_above(0.0).size()) == (kRows.size())); + CHECK((sparse.indices_above(-1.0).size()) == (kStateLength)); + CHECK(sparse.indices_above(1.0).empty()); + CHECK(sparse.indices_above(std::numeric_limits::quiet_NaN()).empty()); } // End-to-end sparse-vs-dense equivalence, with no synthetic operator involved: the gradient path takes // its value from the dense inner_product over its back-evolution buffer, the energy path from the sparse // dot over the very same forward-evolved operator, so the two must agree bit-exactly -- on the exact // graph and on the pared one, in both pictures. -BOOST_AUTO_TEST_CASE(sparse_energy_matches_the_dense_gradient_value_bit_exactly) { +TEST_CASE("sparse_energy_matches_the_dense_gradient_value_bit_exactly") { constexpr size_t kNumModes = 8; const auto data = test_utils::load_case_data("random_exact.msgpack"); for (const auto schrodinger_cutoff : {std::optional{}, std::optional{4}}) { - BOOST_TEST_CONTEXT("schrodinger_cutoff = " << (schrodinger_cutoff ? "4" : "none")) { + { + INFO("schrodinger_cutoff = " << (schrodinger_cutoff ? "4" : "none")); test_utils::SimulatorConfig cfg{.schrodinger_cutoff = schrodinger_cutoff, .comm = MPI_COMM_SELF}; auto sim = test_utils::build_simulator(data, cfg); sim.build_graph(data.majoranas, data.param_inds, data.gen_coeffs); - BOOST_CHECK_EQUAL(sim.expectation_value(data.parameters), - sim.expectation_value_and_gradient(data.parameters).first); + CHECK((sim.expectation_value(data.parameters)) + == (sim.expectation_value_and_gradient(data.parameters).first)); // Paring drives the keep-set off the sparse scores in the Heisenberg picture, so this also // pins EvalState::indices_above against the dense scan through the real functional. const std::optional threshold{1e-10}; - BOOST_CHECK_EQUAL(sim.expectation_value_functional(threshold)(data.parameters), - sim.expectation_value_and_gradient_functional(threshold)(data.parameters).first); + CHECK((sim.expectation_value_functional(threshold)(data.parameters)) + == (sim.expectation_value_and_gradient_functional(threshold)(data.parameters).first)); } } } @@ -285,7 +290,7 @@ BOOST_AUTO_TEST_CASE(sparse_energy_matches_the_dense_gradient_value_bit_exactly) // End-to-end counterpart of the scatter_into contract: the gradient's dense state lives in thread-local // scratch shared by every functional on the thread, so two propagators of different operator sizes // interleaving gradient calls must each keep reproducing their isolated value exactly. -BOOST_AUTO_TEST_CASE(interleaved_gradients_do_not_share_scratch_state) { +TEST_CASE("interleaved_gradients_do_not_share_scratch_state") { constexpr size_t kNumModes = 8; const auto data = test_utils::load_case_data("random_exact.msgpack"); @@ -298,7 +303,7 @@ BOOST_AUTO_TEST_CASE(interleaved_gradients_do_not_share_scratch_state) { auto wide = build(2 * kNumModes); auto narrow = build(4); - BOOST_REQUIRE(wide.mp_op().size() != narrow.mp_op().size()); + REQUIRE(wide.mp_op().size() != narrow.mp_op().size()); auto grad_wide = wide.expectation_value_and_gradient_functional(); auto grad_narrow = narrow.expectation_value_and_gradient_functional(); @@ -306,26 +311,27 @@ BOOST_AUTO_TEST_CASE(interleaved_gradients_do_not_share_scratch_state) { const auto [ref_wide_value, ref_wide_grad] = grad_wide(data.parameters); const auto [ref_narrow_value, ref_narrow_grad] = grad_narrow(data.parameters); for (int round = 0; round < 2; ++round) { - BOOST_TEST_CONTEXT("round " << round) { + { + INFO("round " << round); const auto [wide_value, wide_grad] = grad_wide(data.parameters); - BOOST_CHECK_EQUAL(wide_value, ref_wide_value); - BOOST_CHECK(wide_grad == ref_wide_grad); + CHECK((wide_value) == (ref_wide_value)); + CHECK(wide_grad == ref_wide_grad); const auto [narrow_value, narrow_grad] = grad_narrow(data.parameters); - BOOST_CHECK_EQUAL(narrow_value, ref_narrow_value); - BOOST_CHECK(narrow_grad == ref_narrow_grad); + CHECK((narrow_value) == (ref_narrow_value)); + CHECK(narrow_grad == ref_narrow_grad); } } // The sparse energy path leaves no dense state behind on the Heisenberg operator. - BOOST_CHECK_EQUAL(wide.expectation_value_functional()(data.parameters), ref_wide_value); - BOOST_CHECK(wide.mp_op().state_coeffs.empty()); - BOOST_CHECK(narrow.mp_op().state_coeffs.empty()); + CHECK((wide.expectation_value_functional()(data.parameters)) == (ref_wide_value)); + CHECK(wide.mp_op().state_coeffs.empty()); + CHECK(narrow.mp_op().state_coeffs.empty()); } -BOOST_AUTO_TEST_CASE(eval_state_sparse_rejects_inconsistent_inputs) { - BOOST_CHECK_THROW(EvalState::sparse(kStateLength, kRows, VecD{1.0}), std::invalid_argument); +TEST_CASE("eval_state_sparse_rejects_inconsistent_inputs") { + CHECK_THROWS_AS(EvalState::sparse(kStateLength, kRows, VecD{1.0}), std::invalid_argument); const std::vector out_of_range = {0, static_cast(kStateLength)}; - BOOST_CHECK_THROW(EvalState::sparse(kStateLength, out_of_range, VecD{1.0, 1.0}), std::invalid_argument); - BOOST_CHECK_NO_THROW(EvalState::sparse(0, {}, {})); + CHECK_THROWS_AS(EvalState::sparse(kStateLength, out_of_range, VecD{1.0, 1.0}), std::invalid_argument); + CHECK_NOTHROW(EvalState::sparse(0, {}, {})); } diff --git a/cpp/tests/mpi_distributed_layer_equivalence.cpp b/cpp/tests/mpi_distributed_layer_equivalence.cpp index 152eb2b7..46990c5f 100644 --- a/cpp/tests/mpi_distributed_layer_equivalence.cpp +++ b/cpp/tests/mpi_distributed_layer_equivalence.cpp @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include +#include +#include +#include #include #include @@ -61,21 +63,21 @@ auto run_energy(const TestInputs& inputs, MPI_Comm comm) -> double { return fn(inputs.data.parameters); } -BOOST_AUTO_TEST_CASE(rank_count_energy_within_fp_tolerance) { +TEST_CASE("rank_count_energy_within_fp_tolerance") { if (mpi::size(MPI_COMM_WORLD) < 2) { - BOOST_TEST_MESSAGE("Skipping cross-rank-count case: requires at least 2 ranks."); + INFO("Skipping cross-rank-count case: requires at least 2 ranks."); return; } const auto inputs = load_inputs(); const double e_serial = run_energy(inputs, MPI_COMM_SELF); const double e_world = run_energy(inputs, MPI_COMM_WORLD); - BOOST_TEST_MESSAGE("serial=" << e_serial << " world=" << e_world << " diff=" << (e_world - e_serial)); - BOOST_TEST(near(e_serial, e_world)); + INFO("serial=" << e_serial << " world=" << e_world << " diff=" << (e_world - e_serial)); + CHECK(near(e_serial, e_world)); } -BOOST_AUTO_TEST_CASE(gradient_rank_count_within_fp_tolerance) { +TEST_CASE("gradient_rank_count_within_fp_tolerance") { if (mpi::size(MPI_COMM_WORLD) < 2) { - BOOST_TEST_MESSAGE("Skipping gradient cross-rank-count case: requires at least 2 ranks."); + INFO("Skipping gradient cross-rank-count case: requires at least 2 ranks."); return; } const auto& inputs = load_inputs(); @@ -98,10 +100,11 @@ BOOST_AUTO_TEST_CASE(gradient_rank_count_within_fp_tolerance) { const auto g_serial = run_gradient(MPI_COMM_SELF); const auto g_world = run_gradient(MPI_COMM_WORLD); - BOOST_REQUIRE_EQUAL(g_serial.size(), g_world.size()); + REQUIRE((g_serial.size()) == (g_world.size())); for (size_t i = 0; i < g_serial.size(); ++i) { - BOOST_TEST_CONTEXT("gradient idx=" << i) { - BOOST_TEST(near(g_serial[i], g_world[i])); + { + INFO("gradient idx=" << i); + CHECK(near(g_serial[i], g_world[i])); } } } @@ -149,15 +152,15 @@ auto run_pauli_energy(MPI_Comm comm) -> double { return sim.expectation_value({}); } -BOOST_AUTO_TEST_CASE(pauli_rank_count_energy_within_fp_tolerance) { +TEST_CASE("pauli_rank_count_energy_within_fp_tolerance") { if (mpi::size(MPI_COMM_WORLD) < 2) { - BOOST_TEST_MESSAGE("Skipping Pauli cross-rank-count case: requires at least 2 ranks."); + INFO("Skipping Pauli cross-rank-count case: requires at least 2 ranks."); return; } const double e_serial = run_pauli_energy(MPI_COMM_SELF); const double e_world = run_pauli_energy(MPI_COMM_WORLD); - BOOST_TEST_MESSAGE("pauli serial=" << e_serial << " world=" << e_world); - BOOST_TEST(near(e_serial, e_world)); + INFO("pauli serial=" << e_serial << " world=" << e_world); + CHECK(near(e_serial, e_world)); } // MPI x partition hybrid: partitions=S under R ranks builds the HybridComm flat R*S world, which only changes @@ -184,9 +187,9 @@ auto run_energy_partitioned(const TestInputs& inputs, MPI_Comm comm, size_t part return {e, sim.size()}; } -BOOST_AUTO_TEST_CASE(hybrid_mpi_partition_energy_and_size_equivalence) { +TEST_CASE("hybrid_mpi_partition_energy_and_size_equivalence") { if (mpi::size(MPI_COMM_WORLD) < 2) { - BOOST_TEST_MESSAGE("Skipping hybrid case: requires at least 2 ranks."); + INFO("Skipping hybrid case: requires at least 2 ranks."); return; } const auto inputs = load_inputs(); @@ -194,10 +197,10 @@ BOOST_AUTO_TEST_CASE(hybrid_mpi_partition_energy_and_size_equivalence) { const auto [e_hybrid, n_local] = run_energy_partitioned(inputs, MPI_COMM_WORLD, 2); // Each rank's facade holds only its local partitions; the global term count is the cross-rank sum. const size_t n_hybrid_global = mpi::allreduce_sum(n_local, MPI_COMM_WORLD); - BOOST_TEST_MESSAGE("serial=" << e_serial << " (n=" << n_serial << ") hybrid R*2=" << e_hybrid - << " (global n=" << n_hybrid_global << ")"); - BOOST_TEST(near(e_serial, e_hybrid)); - BOOST_CHECK_EQUAL(n_serial, n_hybrid_global); + INFO("serial=" << e_serial << " (n=" << n_serial << ") hybrid R*2=" << e_hybrid << " (global n=" << n_hybrid_global + << ")"); + CHECK(near(e_serial, e_hybrid)); + CHECK((n_serial) == (n_hybrid_global)); } } // namespace diff --git a/cpp/tests/mpi_fresh_insert_equivalence.cpp b/cpp/tests/mpi_fresh_insert_equivalence.cpp index 2973131b..0702bc4e 100644 --- a/cpp/tests/mpi_fresh_insert_equivalence.cpp +++ b/cpp/tests/mpi_fresh_insert_equivalence.cpp @@ -18,7 +18,9 @@ // mpi_distributed_layer_equivalence. Only runs at world >= 2. Oracle: serial<->world equivalence -- // the deterministic base+j miss-prefix must sum the same terms at any rank count, to near()'s rtol. -#include +#include +#include +#include #include #include @@ -54,15 +56,15 @@ auto run_schrodinger_majorana(const CaseData& data, MPI_Comm comm) -> double { return energy_fn(VecD{}); } -BOOST_FIXTURE_TEST_CASE(mpi_fresh_insert_schrodinger_majorana_serial_world_equiv, ExampleDataFix) { +TEST_CASE_METHOD(ExampleDataFix, "mpi_fresh_insert_schrodinger_majorana_serial_world_equiv") { if (mpi::size(MPI_COMM_WORLD) < 2) { - BOOST_TEST_MESSAGE("Skipping Schrödinger Majorana fresh-insert equivalence (world size = 1)."); + INFO("Skipping Schrödinger Majorana fresh-insert equivalence (world size = 1)."); return; } const double e_serial = run_schrodinger_majorana(data, MPI_COMM_SELF); const double e_world = run_schrodinger_majorana(data, MPI_COMM_WORLD); - BOOST_TEST_MESSAGE("schrodinger majorana serial=" << e_serial << " world=" << e_world); - BOOST_TEST(near(e_serial, e_world)); + INFO("schrodinger majorana serial=" << e_serial << " world=" << e_world); + CHECK(near(e_serial, e_world)); } // Drives the pauli_state_phase sub-branch of the same miss arm: a hand Pauli operator with X / ZZ @@ -107,15 +109,15 @@ auto run_schrodinger_pauli(MPI_Comm comm) -> double { return sim.expectation_value({}); } -BOOST_AUTO_TEST_CASE(mpi_fresh_insert_schrodinger_pauli_serial_world_equiv) { +TEST_CASE("mpi_fresh_insert_schrodinger_pauli_serial_world_equiv") { if (mpi::size(MPI_COMM_WORLD) < 2) { - BOOST_TEST_MESSAGE("Skipping Schrödinger Pauli fresh-insert equivalence (world size = 1)."); + INFO("Skipping Schrödinger Pauli fresh-insert equivalence (world size = 1)."); return; } const double e_serial = run_schrodinger_pauli(MPI_COMM_SELF); const double e_world = run_schrodinger_pauli(MPI_COMM_WORLD); - BOOST_TEST_MESSAGE("schrodinger pauli serial=" << e_serial << " world=" << e_world); - BOOST_TEST(near(e_serial, e_world)); + INFO("schrodinger pauli serial=" << e_serial << " world=" << e_world); + CHECK(near(e_serial, e_world)); } } // namespace diff --git a/cpp/tests/mpi_pare.cpp b/cpp/tests/mpi_pare.cpp index 0e57f162..4d1f25c1 100644 --- a/cpp/tests/mpi_pare.cpp +++ b/cpp/tests/mpi_pare.cpp @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include +#include +#include +#include #include @@ -20,10 +22,10 @@ using namespace test_utils; -BOOST_AUTO_TEST_CASE(multi_rank_pare_expval_is_finite) { +TEST_CASE("multi_rank_pare_expval_is_finite") { const int world_size = monoprop::mpi::size(MPI_COMM_WORLD); if (world_size < 2) { - BOOST_TEST_MESSAGE("Skipping multi_rank_pare_expval_is_finite: MPI world size=" << world_size); + INFO("Skipping multi_rank_pare_expval_is_finite: MPI world size=" << world_size); return; } @@ -36,14 +38,15 @@ BOOST_AUTO_TEST_CASE(multi_rank_pare_expval_is_finite) { auto baseline_sim = build_simulator(data, cfg); const double baseline_expval = evaluate_expval(baseline_sim, data, false); - BOOST_CHECK_SMALL(std::abs(baseline_expval - data.actual_expval), kExpvalAtol); + CHECK_THAT(std::abs(baseline_expval - data.actual_expval), Catch::Matchers::WithinAbs(0.0, kExpvalAtol)); auto pared_sim = build_simulator(data, cfg); const double pared_expval = evaluate_expval(pared_sim, data, true); - BOOST_TEST_CONTEXT("world_size=" << world_size) { - BOOST_CHECK_MESSAGE(std::isfinite(pared_expval), - "pare=true produced non-finite expectation value across MPI ranks"); - BOOST_CHECK_SMALL(std::abs(pared_expval - data.actual_expval), kExpvalAtol); + { + INFO("world_size=" << world_size); + INFO("pare=true produced non-finite expectation value across MPI ranks"); + CHECK(std::isfinite(pared_expval)); + CHECK_THAT(std::abs(pared_expval - data.actual_expval), Catch::Matchers::WithinAbs(0.0, kExpvalAtol)); } } diff --git a/cpp/tests/mpi_utils_tests.cpp b/cpp/tests/mpi_utils_tests.cpp index 8372e88f..79b45515 100644 --- a/cpp/tests/mpi_utils_tests.cpp +++ b/cpp/tests/mpi_utils_tests.cpp @@ -14,7 +14,9 @@ // The pure MPIUtils.h primitives (term->owner mapping, wire word packing), driven without a comm. -#include +#include +#include +#include #include #include @@ -24,7 +26,7 @@ using namespace monoprop; -BOOST_AUTO_TEST_CASE(mpi_utils_find_rank_range_and_hash_mod) { +TEST_CASE("mpi_utils_find_rank_range_and_hash_mod") { constexpr size_t N = 32; std::mt19937_64 rng(0x9E3779B9ULL); std::uniform_int_distribution slot(0, 2 * N - 1); @@ -36,21 +38,21 @@ BOOST_AUTO_TEST_CASE(mpi_utils_find_rank_range_and_hash_mod) { const auto mono = indices_to_bitset(inds); for (size_t n_ranks : {size_t{1}, size_t{2}, size_t{3}, size_t{7}}) { const size_t r = find_rank(mono, n_ranks); - BOOST_TEST(r < n_ranks); - BOOST_TEST(r == monomial_hash(mono) % n_ranks); - BOOST_TEST(r == find_rank(mono, n_ranks)); // deterministic + CHECK(r < n_ranks); + CHECK(r == monomial_hash(mono) % n_ranks); + CHECK(r == find_rank(mono, n_ranks)); // deterministic } } } // n_ranks == 0 is degenerate: owner is rank 0, not a modulo by zero. -BOOST_AUTO_TEST_CASE(mpi_utils_find_rank_zero_ranks) { +TEST_CASE("mpi_utils_find_rank_zero_ranks") { constexpr size_t N = 32; const auto mono = indices_to_bitset(VecZ{0, 3, 5}); - BOOST_TEST(find_rank(mono, 0) == 0U); + CHECK(find_rank(mono, 0) == 0U); } -BOOST_AUTO_TEST_CASE(mpi_utils_monomial_words_roundtrip) { +TEST_CASE("mpi_utils_monomial_words_roundtrip") { constexpr size_t N = 96; // 2N = 192 bits -> 3 words const auto a = indices_to_bitset(VecZ{0, 1, 100, 191}); const auto b = indices_to_bitset(VecZ{5}); @@ -60,16 +62,16 @@ BOOST_AUTO_TEST_CASE(mpi_utils_monomial_words_roundtrip) { mpi_detail::append_monomial_words(a, buf); mpi_detail::append_monomial_words(b, buf); mpi_detail::append_monomial_words(c, buf); - BOOST_REQUIRE(buf.size() == 3 * mpi_detail::kWords); + REQUIRE(buf.size() == 3 * mpi_detail::kWords); - BOOST_TEST((mpi_detail::read_monomial_from_words(buf, 0) == a)); - BOOST_TEST((mpi_detail::read_monomial_from_words(buf, mpi_detail::kWords) == b)); - BOOST_TEST((mpi_detail::read_monomial_from_words(buf, 2 * mpi_detail::kWords) == c)); + CHECK((mpi_detail::read_monomial_from_words(buf, 0) == a)); + CHECK((mpi_detail::read_monomial_from_words(buf, mpi_detail::kWords) == b)); + CHECK((mpi_detail::read_monomial_from_words(buf, 2 * mpi_detail::kWords) == c)); constexpr size_t M = 32; const auto d = indices_to_bitset(VecZ{2, 40, 63}); VecZ sbuf; mpi_detail::append_monomial_words(d, sbuf); - BOOST_REQUIRE(sbuf.size() == mpi_detail::kWords); - BOOST_TEST((mpi_detail::read_monomial_from_words(sbuf, 0) == d)); + REQUIRE(sbuf.size() == mpi_detail::kWords); + CHECK((mpi_detail::read_monomial_from_words(sbuf, 0) == d)); } diff --git a/cpp/tests/operator_index_tests.cpp b/cpp/tests/operator_index_tests.cpp index 486776c7..590d22a3 100644 --- a/cpp/tests/operator_index_tests.cpp +++ b/cpp/tests/operator_index_tests.cpp @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include +#include +#include +#include #include #include @@ -27,13 +29,13 @@ using namespace monoprop; using namespace monoprop::detail; -BOOST_AUTO_TEST_CASE(operator_index_term_index_width_matches_build) { +TEST_CASE("operator_index_term_index_width_matches_build") { #if defined(monoprop_WIDE_TERM_INDEX) static_assert(sizeof(TermIndex) == 8, "wide build must use 64-bit TermIndex"); - BOOST_TEST(sizeof(TermIndex) == 8u); + CHECK(sizeof(TermIndex) == 8u); #else static_assert(sizeof(TermIndex) == 4, "default build must use 32-bit TermIndex"); - BOOST_TEST(sizeof(TermIndex) == 4u); + CHECK(sizeof(TermIndex) == 4u); #endif } @@ -52,51 +54,51 @@ MSet bs(const VecZ &r) { } } // namespace -BOOST_AUTO_TEST_CASE(rows_roundtrip_dense_popcount_positions) { +TEST_CASE("rows_roundtrip_dense_popcount_positions") { Store s; s.push_back(bs({0, 3, 5})); s.push_back(bs({1, 2})); - BOOST_TEST(s.size() == 2u); - BOOST_TEST(s.popcount(0) == 3u); - BOOST_TEST(s.popcount(1) == 2u); - BOOST_TEST((s.row(0) == bs({0, 3, 5}))); + CHECK(s.size() == 2u); + CHECK(s.popcount(0) == 3u); + CHECK(s.popcount(1) == 2u); + CHECK((s.row(0) == bs({0, 3, 5}))); std::vector pos; s.for_each_position(0, [&](size_t b) { pos.push_back(b); }); - BOOST_TEST(pos.size() == 3u); + CHECK(pos.size() == 3u); // for_each_position yields raw bit positions (ascending). indices_to_bitset<32>({0,3,5}) // sets bits at 2*32-1-0=63, 2*32-1-3=60, 2*32-1-5=58, so find_first gives 58 first. - BOOST_TEST(pos[0] == 58u); - BOOST_TEST(pos[2] == 63u); + CHECK(pos[0] == 58u); + CHECK(pos[2] == 63u); } -BOOST_AUTO_TEST_CASE(index_emplace_then_find_roundtrip) { +TEST_CASE("index_emplace_then_find_roundtrip") { Store s; s.push_back(bs({0, 3, 5})); s.emplace(bs({0, 3, 5}), 0); s.push_back(bs({1, 2})); s.emplace(bs({1, 2}), 1); auto f = s.find(bs({1, 2})); - BOOST_TEST(f.has_value()); - BOOST_TEST(*f == 1u); - BOOST_TEST(!s.find(bs({7, 9})).has_value()); + CHECK(f.has_value()); + CHECK(*f == 1u); + CHECK(!s.find(bs({7, 9})).has_value()); } -BOOST_AUTO_TEST_CASE(width_is_a_construction_invariant) { +TEST_CASE("width_is_a_construction_invariant") { Store s(4); // stride = 1 + 4, fixed at construction s.push_back(bs({0, 2, 4, 6})); // a 4-position row fits inline at width 4 s.reserve(20); // capacity only -- width/stride are never touched by reserve - BOOST_TEST(s.popcount(0) == 4u); - BOOST_TEST((s.row(0) == bs({0, 2, 4, 6}))); + CHECK(s.popcount(0) == 4u); + CHECK((s.row(0) == bs({0, 2, 4, 6}))); } -BOOST_AUTO_TEST_CASE(overflow_is_lossless_above_width) { +TEST_CASE("overflow_is_lossless_above_width") { Store s(2); // width 2; a 3-position row must overflow s.push_back(bs({0, 1, 2})); - BOOST_TEST(s.popcount(0) == 3u); // popcount recovered from the overflow map - BOOST_TEST((s.row(0) == bs({0, 1, 2}))); + CHECK(s.popcount(0) == 3u); // popcount recovered from the overflow map + CHECK((s.row(0) == bs({0, 1, 2}))); } -BOOST_AUTO_TEST_CASE(index_survives_rehash_in_place) { +TEST_CASE("index_survives_rehash_in_place") { Store a; // 64 distinct rows (positions i and (i+7)%62) force at least one rehash of the in-place index. for (int i = 0; i < 64; ++i) { @@ -104,11 +106,11 @@ BOOST_AUTO_TEST_CASE(index_survives_rehash_in_place) { a.emplace(a.row(static_cast(i)), static_cast(i)); } auto f = a.find(a.row(50)); - BOOST_TEST(f.has_value()); - BOOST_TEST(*f == 50u); + CHECK(f.has_value()); + CHECK(*f == 50u); } -BOOST_AUTO_TEST_CASE(clone_is_deep_and_independent) { +TEST_CASE("clone_is_deep_and_independent") { Store a(4); // non-default width must carry over a.push_back(bs({0, 3, 5})); a.emplace(bs({0, 3, 5}), 0); @@ -116,41 +118,41 @@ BOOST_AUTO_TEST_CASE(clone_is_deep_and_independent) { a.emplace(bs({1, 2}), 1); auto b = a.clone(); - BOOST_TEST(b->size() == 2u); - BOOST_TEST((b->row(0) == bs({0, 3, 5}))); + CHECK(b->size() == 2u); + CHECK((b->row(0) == bs({0, 3, 5}))); auto f = b->find(bs({1, 2})); - BOOST_TEST(f.has_value()); - BOOST_TEST(*f == 1u); + CHECK(f.has_value()); + CHECK(*f == 1u); a.push_back(bs({6, 7})); a.emplace(bs({6, 7}), 2); - BOOST_TEST(b->size() == 2u); - BOOST_TEST(!b->find(bs({6, 7})).has_value()); + CHECK(b->size() == 2u); + CHECK(!b->find(bs({6, 7})).has_value()); // If the clone still referenced the source's rows, this find would read a->row(0) (now {8,9}) // and fail. a.set(0, bs({8, 9})); auto g = b->find(bs({0, 3, 5})); - BOOST_TEST(g.has_value()); - BOOST_TEST(*g == 0u); + CHECK(g.has_value()); + CHECK(*g == 0u); } -BOOST_AUTO_TEST_CASE(clone_preserves_overflow_rows) { +TEST_CASE("clone_preserves_overflow_rows") { Store a(2); // width 2; a 3-position row overflows losslessly a.push_back(bs({0, 1, 2})); a.emplace(bs({0, 1, 2}), 0); auto b = a.clone(); - BOOST_TEST(b->popcount(0) == 3u); - BOOST_TEST((b->row(0) == bs({0, 1, 2}))); - BOOST_TEST(*b->find(bs({0, 1, 2})) == 0u); + CHECK(b->popcount(0) == 3u); + CHECK((b->row(0) == bs({0, 1, 2}))); + CHECK(*b->find(bs({0, 1, 2})) == 0u); } // find_batch (the group-prefetch pipelined lookup) must be semantically identical to n independent // find() calls. The query mix below spans several G=16 groups plus a short tail and interleaves // present and absent keys, so every branch but the h32-collision fallback runs; that one needs a // real 32-bit hash collision, but the equivalence assertion pins it whichever path a key takes. -BOOST_AUTO_TEST_CASE(find_batch_matches_scalar_find) { +TEST_CASE("find_batch_matches_scalar_find") { Store s; constexpr size_t kRows = 200; // > 12 groups of G=16 // (i/60, 4 + i%60) is a bijection for i < 240 over the disjoint ranges {0..3} and {4..63}. @@ -166,7 +168,7 @@ BOOST_AUTO_TEST_CASE(find_batch_matches_scalar_find) { queries.push_back(bs({0, 1, 2 + (i % 20)})); } queries.push_back(bs({0, 1, 2})); // 401 total - BOOST_TEST(queries.size() % 16u != 0u); + CHECK(queries.size() % 16u != 0u); std::vector out(queries.size(), 424242); s.find_batch(queries.data(), queries.size(), out.data()); @@ -179,18 +181,18 @@ BOOST_AUTO_TEST_CASE(find_batch_matches_scalar_find) { all_match = false; } } - BOOST_TEST(all_match); - BOOST_TEST(out[0] == 0u); // first present key -> row 0 - BOOST_TEST(out[1] == Store::kNotFound); // first absent key + CHECK(all_match); + CHECK(out[0] == 0u); // first present key -> row 0 + CHECK(out[1] == Store::kNotFound); // first absent key } // Pins find_batch's partition.count == 0 early-out. -BOOST_AUTO_TEST_CASE(find_batch_on_empty_store_is_all_missing) { +TEST_CASE("find_batch_on_empty_store_is_all_missing") { Store s; const std::array keys{bs({0, 3}), bs({1, 2}), bs({4, 5, 6})}; std::array out{0, 0, 0}; s.find_batch(keys.data(), keys.size(), out.data()); for (size_t i = 0; i < keys.size(); ++i) { - BOOST_TEST(out[i] == Store::kNotFound); + CHECK(out[i] == Store::kNotFound); } } diff --git a/cpp/tests/pare_graph_tests.cpp b/cpp/tests/pare_graph_tests.cpp index 9e3464d2..81ee2cf2 100644 --- a/cpp/tests/pare_graph_tests.cpp +++ b/cpp/tests/pare_graph_tests.cpp @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include +#include +#include +#include #include #include @@ -51,7 +53,7 @@ auto recompute_cos(const monoprop::detail::InvertedIndex &inverted_ind } // namespace // The streaming pare sweep must engage pruned_cos on exactly the layers whose cos loses an index. -BOOST_AUTO_TEST_CASE(pare_graph_emits_expected_layer_kinds) { +TEST_CASE("pare_graph_emits_expected_layer_kinds") { const auto data = load_case_data("random_exact.msgpack"); SimulatorConfig cfg{.comm = MPI_COMM_SELF}; @@ -61,14 +63,14 @@ BOOST_AUTO_TEST_CASE(pare_graph_emits_expected_layer_kinds) { const auto &graph = sim.graph(); const auto &inverted_index = sim.mp_op().inverted_index(); const VecD state = sim.mp_op().materialize_state(); - BOOST_REQUIRE(state.size() > 0); + REQUIRE(state.size() > 0); // materialize_state() hands back a caller-owned vector and caches nothing on the operator, so // state_coeffs stays empty and the sparse entry count must equal the dense vector's nonzero count. - BOOST_CHECK(sim.mp_op().state_coeffs.empty()); + CHECK(sim.mp_op().state_coeffs.empty()); const auto sparse = sim.mp_op().sparse_state(); - BOOST_CHECK_EQUAL(sparse.rows.size(), - static_cast(std::ranges::count_if(state, [](double c) { return c != 0.0; }))); + CHECK((sparse.rows.size()) + == (static_cast(std::ranges::count_if(state, [](double c) { return c != 0.0; })))); // Single-rank, mark_replayed_d_targets force-keeps every cosine index, so a real threshold prunes // nothing (real pruning is a multi-rank effect, covered by mpi_pare). To reach the prune path @@ -106,7 +108,7 @@ BOOST_AUTO_TEST_CASE(pare_graph_emits_expected_layer_kinds) { } auto pared = pare_graph(graph, seed, local_index_count, /*schrodinger=*/false, MPI_COMM_SELF, provider); - BOOST_REQUIRE_EQUAL(pared.layers(), graph.layers()); + REQUIRE((pared.layers()) == (graph.layers())); size_t pruned_count = 0; for (size_t i = 0; i < pared.layers(); ++i) { @@ -115,7 +117,7 @@ BOOST_AUTO_TEST_CASE(pare_graph_emits_expected_layer_kinds) { // The synthetic index is the only one outside the keep-set, so a stored cos must be the // recomputed one minus exactly that index. `<=` here would also pass on a sweep that // dropped real indices. - BOOST_CHECK_EQUAL(pruned->total_count + 1, provider(i).total_count); + CHECK((pruned->total_count + 1) == (provider(i).total_count)); // Same block-mask walk graph_data() uses to turn a stored cos back into indices: the // decoded set must be the recomputed one with only synth_index missing, so a stored cos @@ -131,17 +133,17 @@ BOOST_AUTO_TEST_CASE(pare_graph_emits_expected_layer_kinds) { const auto kept = decode(*pruned); auto expected = decode(provider(i)); std::erase(expected, synth_index); - BOOST_CHECK_EQUAL(kept.size(), pruned->total_count); - BOOST_TEST(kept == expected, boost::test_tools::per_element()); + CHECK((kept.size()) == (pruned->total_count)); + CHECK(kept == expected); } } // Exactly the marked layer is pruned: every other layer's cos lies entirely inside the keep-set, // and a preserved layer stores nothing (its cos is recomputed at replay). - BOOST_CHECK_EQUAL(pruned_count, 1u); - BOOST_TEST(pared.get_layer(marked_layer).pruned_cos() != static_cast(nullptr)); + CHECK((pruned_count) == (1u)); + CHECK(pared.get_layer(marked_layer).pruned_cos() != static_cast(nullptr)); } -BOOST_AUTO_TEST_CASE(pare_graph_energy_matches_unpared) { +TEST_CASE("pare_graph_energy_matches_unpared") { const auto data = load_case_data("random_exact.msgpack"); SimulatorConfig cfg{.comm = MPI_COMM_SELF}; @@ -157,11 +159,11 @@ BOOST_AUTO_TEST_CASE(pare_graph_energy_matches_unpared) { sim_tiny.build_graph(data.majoranas, data.param_inds, data.gen_coeffs); auto ev_tiny = sim_tiny.expectation_value_functional(std::optional{1e-12}); const double e_tiny = ev_tiny(data.parameters); - BOOST_CHECK_SMALL(std::abs(e_full - e_tiny), 1e-12); + CHECK_THAT(std::abs(e_full - e_tiny), Catch::Matchers::WithinAbs(0.0, 1e-12)); auto sim_real = build_simulator(data, cfg); sim_real.build_graph(data.majoranas, data.param_inds, data.gen_coeffs); auto ev_real = sim_real.expectation_value_functional(std::optional{1e-10}); const double e_real = ev_real(data.parameters); - BOOST_CHECK_SMALL(std::abs(e_real - data.actual_expval), 1e-9); + CHECK_THAT(std::abs(e_real - data.actual_expval), Catch::Matchers::WithinAbs(0.0, 1e-9)); } diff --git a/cpp/tests/partition_equivalence_tests.cpp b/cpp/tests/partition_equivalence_tests.cpp index 9c8a984e..03bcca6f 100644 --- a/cpp/tests/partition_equivalence_tests.cpp +++ b/cpp/tests/partition_equivalence_tests.cpp @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include +#include +#include +#include #include #include @@ -56,7 +58,7 @@ auto majorana_sim(const CaseData &data, size_t partitions, std::optional partitions); } -BOOST_AUTO_TEST_CASE(partition_majorana_energy_matches_across_partition_counts) { +TEST_CASE("partition_majorana_energy_matches_across_partition_counts") { const auto data = load_case_data("random_exact.msgpack"); auto ref = majorana_sim(data, 1); ref.build_graph(data.majoranas, data.param_inds, data.gen_coeffs); @@ -66,15 +68,16 @@ BOOST_AUTO_TEST_CASE(partition_majorana_energy_matches_across_partition_counts) auto sim = majorana_sim(data, S); sim.build_graph(data.majoranas, data.param_inds, data.gen_coeffs); const double e = sim.expectation_value(data.parameters); - BOOST_TEST_CONTEXT("partitions=" << S << " e=" << e << " ref=" << e1) { - BOOST_TEST(near(e1, e)); + { + INFO("partitions=" << S << " e=" << e << " ref=" << e1); + CHECK(near(e1, e)); } // Aggregated operator size is partition-count invariant (terms are hash-partitioned, no overlap). - BOOST_CHECK_EQUAL(ref.size(), sim.size()); + CHECK((ref.size()) == (sim.size())); } } -BOOST_AUTO_TEST_CASE(partition_majorana_gradient_matches_across_partition_counts) { +TEST_CASE("partition_majorana_gradient_matches_across_partition_counts") { const auto data = load_case_data("random_exact.msgpack"); auto ref = majorana_sim(data, 1); ref.build_graph(data.majoranas, data.param_inds, data.gen_coeffs); @@ -84,16 +87,17 @@ BOOST_AUTO_TEST_CASE(partition_majorana_gradient_matches_across_partition_counts auto sim = majorana_sim(data, S); sim.build_graph(data.majoranas, data.param_inds, data.gen_coeffs); const auto g = sim.expectation_value_and_gradient(data.parameters).second; - BOOST_REQUIRE_EQUAL(g1.size(), g.size()); + REQUIRE((g1.size()) == (g.size())); for (size_t i = 0; i < g1.size(); ++i) { - BOOST_TEST_CONTEXT("partitions=" << S << " grad idx=" << i) { - BOOST_TEST(near(g1[i], g[i])); + { + INFO("partitions=" << S << " grad idx=" << i); + CHECK(near(g1[i], g[i])); } } } } -BOOST_AUTO_TEST_CASE(partition_majorana_propagate_then_expectation_matches) { +TEST_CASE("partition_majorana_propagate_then_expectation_matches") { const auto data = load_case_data("random_exact.msgpack"); auto run = [&](size_t S) { auto sim = majorana_sim(data, S); @@ -103,30 +107,31 @@ BOOST_AUTO_TEST_CASE(partition_majorana_propagate_then_expectation_matches) { const auto [e1, n1] = run(1); for (const size_t S : {size_t{2}, size_t{4}}) { const auto [e, n] = run(S); - BOOST_TEST_CONTEXT("partitions=" << S) { - BOOST_TEST(near(e1, e)); - BOOST_CHECK_EQUAL(n1, n); + { + INFO("partitions=" << S); + CHECK(near(e1, e)); + CHECK((n1) == (n)); } } } // Two independent S=4 runs are bit-identical: ShmComm sums in ascending rank order and each partition is // deterministic, so a given partition count has no run-to-run jitter. -BOOST_AUTO_TEST_CASE(partition_energy_is_deterministic) { +TEST_CASE("partition_energy_is_deterministic") { const auto data = load_case_data("random_exact.msgpack"); auto energy_s4 = [&] { auto sim = majorana_sim(data, 4); sim.build_graph(data.majoranas, data.param_inds, data.gen_coeffs); return sim.expectation_value(data.parameters); }; - BOOST_CHECK_EQUAL(energy_s4(), energy_s4()); + CHECK((energy_s4()) == (energy_s4())); } // contract_partially() hands back raw coefficients positioned by the owning partition's own indexing, // so a facade's array is the per-partition blocks concatenated in partition order. What IS invariant is // the multiset: the partitions are disjoint and cover every term. Sorting both sides is the only // comparison the API's contract supports — see the note on contract_partially(). -BOOST_AUTO_TEST_CASE(partition_contract_partially_matches_as_a_multiset) { +TEST_CASE("partition_contract_partially_matches_as_a_multiset") { const auto data = load_case_data("random_exact.msgpack"); auto sorted_coeffs = [&](size_t S) { auto sim = majorana_sim(data, S); @@ -138,10 +143,11 @@ BOOST_AUTO_TEST_CASE(partition_contract_partially_matches_as_a_multiset) { const auto ref = sorted_coeffs(1); for (const size_t S : {size_t{2}, size_t{4}}) { const auto coeffs = sorted_coeffs(S); - BOOST_REQUIRE_EQUAL(ref.size(), coeffs.size()); + REQUIRE((ref.size()) == (coeffs.size())); for (size_t i = 0; i < ref.size(); ++i) { - BOOST_TEST_CONTEXT("partitions=" << S << " sorted idx=" << i) { - BOOST_TEST(near(ref[i], coeffs[i])); + { + INFO("partitions=" << S << " sorted idx=" << i); + CHECK(near(ref[i], coeffs[i])); } } } @@ -149,19 +155,19 @@ BOOST_AUTO_TEST_CASE(partition_contract_partially_matches_as_a_multiset) { // The raw per-partition accessors have no facade reading: the facade's own graph_/mp_op_ are never // populated, so returning them would hand a C++ consumer empty state that looks valid. -BOOST_AUTO_TEST_CASE(partition_raw_accessors_reject_a_facade) { +TEST_CASE("partition_raw_accessors_reject_a_facade") { const auto data = load_case_data("random_exact.msgpack"); auto sim = majorana_sim(data, 4); sim.build_graph(data.majoranas, data.param_inds, data.gen_coeffs); - BOOST_CHECK_THROW(static_cast(sim.graph()), std::runtime_error); - BOOST_CHECK_THROW(static_cast(sim.mp_op()), std::runtime_error); - BOOST_CHECK_THROW(static_cast(sim.indexing()), std::runtime_error); - BOOST_CHECK_THROW(static_cast(sim.graph_data()), std::runtime_error); + CHECK_THROWS_AS(static_cast(sim.graph()), std::runtime_error); + CHECK_THROWS_AS(static_cast(sim.mp_op()), std::runtime_error); + CHECK_THROWS_AS(static_cast(sim.indexing()), std::runtime_error); + CHECK_THROWS_AS(static_cast(sim.graph_data()), std::runtime_error); auto solo = majorana_sim(data, 1); solo.build_graph(data.majoranas, data.param_inds, data.gen_coeffs); - BOOST_CHECK_GT(solo.graph().layers(), 0U); - BOOST_CHECK_EQUAL(solo.graph_data().size(), solo.graph_layers()); + CHECK((solo.graph().layers()) > (0U)); + CHECK((solo.graph_data().size()) == (solo.graph_layers())); } // A facade owns no operator, so a setter that stopped there would leave every partition on its old @@ -169,7 +175,7 @@ BOOST_AUTO_TEST_CASE(partition_raw_accessors_reject_a_facade) { // during the graph build, so it shows up in the aggregated term count. LiH is used rather than the // random_exact fixture, which is small enough that no setting truncates anything. The tightened // oracle must itself differ from the wide run, else the last assertion would hold vacuously. -BOOST_AUTO_TEST_CASE(partition_setters_reach_every_partition) { +TEST_CASE("partition_setters_reach_every_partition") { constexpr size_t kLihModes = LihFixture::n_modes; const auto data = load_case_data("lih_fermionic_spin_exact.msgpack"); @@ -188,7 +194,7 @@ BOOST_AUTO_TEST_CASE(partition_setters_reach_every_partition) { /*partitions=*/4); if (cutoff != updated_cutoff) { sim.update_cutoff(updated_cutoff); - BOOST_CHECK_EQUAL(sim.cutoff(), updated_cutoff); + CHECK((sim.cutoff()) == (updated_cutoff)); } sim.build_graph(data.majoranas, data.param_inds, data.gen_coeffs); return sim.size(); @@ -196,12 +202,12 @@ BOOST_AUTO_TEST_CASE(partition_setters_reach_every_partition) { const size_t n_wide = build(2 * kLihModes, 2 * kLihModes); const size_t n_tight = build(4, 4); - BOOST_REQUIRE_NE(n_wide, n_tight); + REQUIRE((n_wide) != (n_tight)); // Constructed wide, then tightened: only the partitions' own cutoff can produce the tight count. - BOOST_CHECK_EQUAL(n_tight, build(2 * kLihModes, 4)); + CHECK((n_tight) == (build(2 * kLihModes, 4))); } -BOOST_AUTO_TEST_CASE(partition_deep_copy_matches) { +TEST_CASE("partition_deep_copy_matches") { const auto data = load_case_data("random_exact.msgpack"); auto sim = majorana_sim(data, 4); sim.build_graph(data.majoranas, data.param_inds, data.gen_coeffs); @@ -209,8 +215,8 @@ BOOST_AUTO_TEST_CASE(partition_deep_copy_matches) { MonomialPropagator copy(sim); // clones the partition group (fresh threads + ShmComm) const double e_copy = copy.expectation_value(data.parameters); - BOOST_CHECK_EQUAL(e, e_copy); - BOOST_CHECK_EQUAL(sim.size(), copy.size()); + CHECK((e) == (e_copy)); + CHECK((sim.size()) == (copy.size())); } constexpr size_t kNq = 6; @@ -264,13 +270,14 @@ auto run_pauli_energy(size_t partitions) -> std::pair { return {sim.expectation_value({}), sim.size()}; } -BOOST_AUTO_TEST_CASE(partition_pauli_energy_matches_across_partition_counts) { +TEST_CASE("partition_pauli_energy_matches_across_partition_counts") { const auto [e1, n1] = run_pauli_energy(1); for (const size_t S : {size_t{2}, size_t{4}}) { const auto [e, n] = run_pauli_energy(S); - BOOST_TEST_CONTEXT("pauli partitions=" << S << " e=" << e << " ref=" << e1) { - BOOST_TEST(near(e1, e)); - BOOST_CHECK_EQUAL(n1, n); + { + INFO("pauli partitions=" << S << " e=" << e << " ref=" << e1); + CHECK(near(e1, e)); + CHECK((n1) == (n)); } } } @@ -280,38 +287,38 @@ BOOST_AUTO_TEST_CASE(partition_pauli_energy_matches_across_partition_counts) { // A partition factory that throws must surface the exception, not std::terminate: the ctor starts the // master threads before building the partitions on them (first-touch locality), so the unwind has to join // already-started threads. Every MonomialPropagator ctor validation reaches this path. -BOOST_AUTO_TEST_CASE(partition_factory_exception_propagates_without_terminate) { +TEST_CASE("partition_factory_exception_propagates_without_terminate") { const auto data = load_case_data("random_exact.msgpack"); // logical_num_modes = 0 is rejected by each partition's own constructor, on its own master thread. - BOOST_CHECK_THROW(MonomialPropagator(data.hamiltonian, - kCutoff, - data.initial_state, - std::nullopt, - MPI_COMM_SELF, - std::nullopt, - std::nullopt, - CutoffType::Length, - std::nullopt, - /*logical_num_modes=*/0, - Basis::Majorana, - /*partitions=*/4), - std::runtime_error); + CHECK_THROWS_AS(MonomialPropagator(data.hamiltonian, + kCutoff, + data.initial_state, + std::nullopt, + MPI_COMM_SELF, + std::nullopt, + std::nullopt, + CutoffType::Length, + std::nullopt, + /*logical_num_modes=*/0, + Basis::Majorana, + /*partitions=*/4), + std::runtime_error); // An out-of-range operator index takes the same path, and the group stays usable afterwards. auto bad_op = data.hamiltonian; bad_op[VecZ{2 * kNumModes}] = std::complex(1.0, 0.0); - BOOST_CHECK_THROW(MonomialPropagator(bad_op, - kCutoff, - data.initial_state, - std::nullopt, - MPI_COMM_SELF, - std::nullopt, - std::nullopt, - CutoffType::Length, - std::nullopt, - kNumModes, - Basis::Majorana, - /*partitions=*/4), - std::runtime_error); - BOOST_CHECK_NO_THROW(majorana_sim(data, 4)); + CHECK_THROWS_AS(MonomialPropagator(bad_op, + kCutoff, + data.initial_state, + std::nullopt, + MPI_COMM_SELF, + std::nullopt, + std::nullopt, + CutoffType::Length, + std::nullopt, + kNumModes, + Basis::Majorana, + /*partitions=*/4), + std::runtime_error); + CHECK_NOTHROW(majorana_sim(data, 4)); } diff --git a/cpp/tests/pauli_algebra_tests.cpp b/cpp/tests/pauli_algebra_tests.cpp index 170cf631..b9df5840 100644 --- a/cpp/tests/pauli_algebra_tests.cpp +++ b/cpp/tests/pauli_algebra_tests.cpp @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include +#include +#include +#include #include #include @@ -83,17 +85,17 @@ template } // namespace -BOOST_AUTO_TEST_CASE(pauli_algebra_pair_swap_and_anticommutation) { +TEST_CASE("pauli_algebra_pair_swap_and_anticommutation") { constexpr size_t N = 8; for (size_t n : {size_t{1}, size_t{2}}) { for (const auto &pa : all_strings(n)) { const auto a = native_bitset(pa); - BOOST_TEST((pair_swap(pair_swap(a)) == a)); + CHECK((pair_swap(pair_swap(a)) == a)); for (const auto &pb : all_strings(n)) { const auto b = native_bitset(pb); const bool antic = pauli_anticommutes(a, b); - BOOST_TEST(antic == string_anticommutes(pa, pb)); + CHECK(antic == string_anticommutes(pa, pb)); // Independent dense-matrix check: anticommute iff AB == -BA. const auto ma = matrix_from_string(pa); const auto mb = matrix_from_string(pb); @@ -101,7 +103,7 @@ BOOST_AUTO_TEST_CASE(pauli_algebra_pair_swap_and_anticommutation) { const auto ab = matmul(ma, mb, d); const auto ba = matmul(mb, ma, d); const bool mat_antic = approx_equal(ab, scalar_mul(cd(-1, 0), ba)); - BOOST_TEST(antic == mat_antic); + CHECK(antic == mat_antic); } } } @@ -113,8 +115,8 @@ BOOST_AUTO_TEST_CASE(pauli_algebra_pair_swap_and_anticommutation) { const auto pb = random_string(rng, n); const auto a = native_bitset(pa); const auto b = native_bitset(pb); - BOOST_TEST((pair_swap(pair_swap(a)) == a)); - BOOST_TEST(pauli_anticommutes(a, b) == string_anticommutes(pa, pb)); + CHECK((pair_swap(pair_swap(a)) == a)); + CHECK(pauli_anticommutes(a, b) == string_anticommutes(pa, pb)); } constexpr size_t NW = 40; // 2N = 80 bits -> 2 words: exercises multiword kernels. @@ -123,18 +125,18 @@ BOOST_AUTO_TEST_CASE(pauli_algebra_pair_swap_and_anticommutation) { const auto pb = random_string(rng, NW); const auto a = native_bitset(pa); const auto b = native_bitset(pb); - BOOST_TEST((pair_swap(pair_swap(a)) == a)); - BOOST_TEST(pauli_anticommutes(a, b) == string_anticommutes(pa, pb)); + CHECK((pair_swap(pair_swap(a)) == a)); + CHECK(pauli_anticommutes(a, b) == string_anticommutes(pa, pb)); } } // The encoding is exactly the Jordan-Wigner image: native == change_basis(jw(P), jw_basis). -BOOST_AUTO_TEST_CASE(pauli_algebra_encoding_is_jw_image) { +TEST_CASE("pauli_algebra_encoding_is_jw_image") { constexpr size_t N = 8; for (size_t n : {size_t{1}, size_t{2}}) { const auto basis = jw_basis(n); for (const auto &p : all_strings(n)) { - BOOST_TEST((native_bitset(p) == change_basis(jw_bitset(p), basis))); + CHECK((native_bitset(p) == change_basis(jw_bitset(p), basis))); } } std::mt19937 rng(0x1234ABCDU); @@ -142,11 +144,11 @@ BOOST_AUTO_TEST_CASE(pauli_algebra_encoding_is_jw_image) { const size_t n = 1 + (rng() % 6); const auto basis = jw_basis(n); const auto p = random_string(rng, n); - BOOST_TEST((native_bitset(p) == change_basis(jw_bitset(p), basis))); + CHECK((native_bitset(p) == change_basis(jw_bitset(p), basis))); } } -BOOST_AUTO_TEST_CASE(pauli_algebra_product_phase_vs_brute_force) { +TEST_CASE("pauli_algebra_product_phase_vs_brute_force") { constexpr size_t N = 4; for (size_t n : {size_t{1}, size_t{2}, size_t{3}}) { const size_t d = size_t{1} << n; @@ -167,17 +169,17 @@ BOOST_AUTO_TEST_CASE(pauli_algebra_product_phase_vs_brute_force) { const auto ab = matmul(ma, mb, d); const cd phi = pauli_product_phase(a, b); - BOOST_TEST(std::abs(std::abs(phi) - 1.0) < 1e-12); - BOOST_TEST(approx_equal(ab, scalar_mul(phi, mr))); + CHECK(std::abs(std::abs(phi) - 1.0) < 1e-12); + CHECK(approx_equal(ab, scalar_mul(phi, mr))); if (pauli_anticommutes(a, b)) { const int sign = pauli_emit_sign_antic(a, b); - BOOST_TEST((sign == 1 || sign == -1)); + CHECK((sign == 1 || sign == -1)); // A*B = sign * i * R for anticommuting Hermitian Paulis. - BOOST_TEST(approx_equal(ab, scalar_mul(cd(0, static_cast(sign)), mr))); + CHECK(approx_equal(ab, scalar_mul(cd(0, static_cast(sign)), mr))); // Hot kernel returns the rotation sign = negated raw emit sign. const auto ctx = make_pauli_gen_context(b); - BOOST_TEST(pauli_rotation_sign(ctx, a, r) == -sign); + CHECK(pauli_rotation_sign(ctx, a, r) == -sign); } } } @@ -190,11 +192,11 @@ BOOST_AUTO_TEST_CASE(pauli_algebra_product_phase_vs_brute_force) { const auto a = native_bitset(random_string(rng, NW)); const auto b = native_bitset(random_string(rng, NW)); const auto ctx = make_pauli_gen_context(b); - BOOST_TEST(pauli_rotation_sign(ctx, a, a ^ b) == -pauli_emit_sign_antic(a, b)); + CHECK(pauli_rotation_sign(ctx, a, a ^ b) == -pauli_emit_sign_antic(a, b)); } } -BOOST_AUTO_TEST_CASE(pauli_algebra_cutoff_and_weight_equivalence) { +TEST_CASE("pauli_algebra_cutoff_and_weight_equivalence") { constexpr size_t N = 32; // single word (2N = 64) constexpr size_t logical = 6; const auto basis = jw_basis(logical); @@ -204,27 +206,27 @@ BOOST_AUTO_TEST_CASE(pauli_algebra_cutoff_and_weight_equivalence) { const auto p = random_string(rng, logical); // P on the low qubits 0..logical-1 const auto native = native_bitset(p); const auto via_jw = change_basis(jw_bitset(p), basis); - BOOST_TEST((native == via_jw)); + CHECK((native == via_jw)); for (unsigned int c : {0U, 1U, 2U, 3U, 6U}) { - BOOST_TEST(support_cutoff(native, c, logical) == support_cutoff(via_jw, c, logical)); + CHECK(support_cutoff(native, c, logical) == support_cutoff(via_jw, c, logical)); // Also exercise the whole-register (logical == NumModes) code path. - BOOST_TEST(support_cutoff(native, c) == support_cutoff(via_jw, c)); + CHECK(support_cutoff(native, c) == support_cutoff(via_jw, c)); } size_t true_weight = 0; for (char ch : p) { true_weight += (ch != 'I') ? 1 : 0; } - BOOST_TEST(pauli_weight(native) == true_weight); + CHECK(pauli_weight(native) == true_weight); // is_paired (support_cutoff's xor_sum == 0) detects exactly the Z-only strings. - BOOST_TEST(is_paired(native) == is_z_only(p)); + CHECK(is_paired(native) == is_z_only(p)); } } // Initial-state phase vs brute-force . -BOOST_AUTO_TEST_CASE(pauli_algebra_state_phase) { +TEST_CASE("pauli_algebra_state_phase") { constexpr size_t N = 8; constexpr size_t n = 5; std::mt19937 rng(0xFACE42U); @@ -255,7 +257,7 @@ BOOST_AUTO_TEST_CASE(pauli_algebra_state_phase) { } } const double phase = pauli_state_phase(z_mono, state_mask); - BOOST_TEST(phase == static_cast(expected)); + CHECK(phase == static_cast(expected)); const size_t d = size_t{1} << n; size_t idx = 0; @@ -265,7 +267,7 @@ BOOST_AUTO_TEST_CASE(pauli_algebra_state_phase) { } } const auto mz = matrix_from_string(pz); - BOOST_TEST(std::abs(mz[idx * d + idx] - cd(static_cast(expected), 0)) < 1e-9); + CHECK(std::abs(mz[idx * d + idx] - cd(static_cast(expected), 0)) < 1e-9); // Non-diagonal Pauli: == 0 (documents why the state-phase guard is Z-only). std::string pnd = random_string(rng, n); @@ -273,6 +275,6 @@ BOOST_AUTO_TEST_CASE(pauli_algebra_state_phase) { pnd[rng() % n] = 'X'; // force at least one off-diagonal letter } const auto mnd = matrix_from_string(pnd); - BOOST_TEST(std::abs(mnd[idx * d + idx]) < 1e-9); + CHECK(std::abs(mnd[idx * d + idx]) < 1e-9); } } diff --git a/cpp/tests/pauli_build_layer_tests.cpp b/cpp/tests/pauli_build_layer_tests.cpp index af069fcd..15796e3d 100644 --- a/cpp/tests/pauli_build_layer_tests.cpp +++ b/cpp/tests/pauli_build_layer_tests.cpp @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include +#include +#include +#include #include #include @@ -146,8 +148,9 @@ auto check_pauli_gate(const std::map &obs, const std::strin } } const auto ref = matmul(matmul(Ud, O, d), U, d); - BOOST_TEST_CONTEXT("N=" << N << " G=" << gstr << " g=" << g << " theta=" << theta) { - BOOST_TEST(approx_equal(engine, ref)); + { + INFO("N=" << N << " G=" << gstr << " g=" << g << " theta=" << theta); + CHECK(approx_equal(engine, ref)); } } @@ -256,7 +259,7 @@ auto build_jw_sim(const std::map &obs, } // namespace // Pins the emit sign. -BOOST_AUTO_TEST_CASE(pauli_build_layer_dense_matrix_ground_truth) { +TEST_CASE("pauli_build_layer_dense_matrix_ground_truth") { const std::map o2{{"XY", 0.5}, {"ZZ", -0.3}, {"YX", 0.7}, {"IZ", 0.2}, {"YY", -0.15}}; for (double th : {0.37, 0.8, 1.3, -0.6}) { check_pauli_gate<2>(o2, "XX", 1.0, th); @@ -322,7 +325,7 @@ auto heisenberg_expval(MonomialPropagator &sim) -> double { // For the same observable and gates, the native Pauli propagator and the JW-image Majorana propagator // must agree on expectation value and term count, across pictures/cutoffs/atol. -BOOST_AUTO_TEST_CASE(pauli_build_layer_jw_isomorphism) { +TEST_CASE("pauli_build_layer_jw_isomorphism") { constexpr size_t N = 3; // Kicked-Ising-like: single-qubit X rotations (incl. odd-popcount generators) + ZZ rotations. PauliCircuit circ; @@ -361,9 +364,10 @@ BOOST_AUTO_TEST_CASE(pauli_build_layer_jw_isomorphism) { jw.build_graph(jw_majs, circ.param_map, jw_gcs); const double en = nat.expectation_value(circ.params); const double ej = jw.expectation_value(circ.params); - BOOST_TEST_CONTEXT(cf.name) { - BOOST_TEST(en == ej, boost::test_tools::tolerance(1e-9)); - BOOST_TEST(nat.size() == jw.size()); + { + INFO(cf.name); + CHECK((en) == Catch::Approx(ej).epsilon(1e-9)); + CHECK(nat.size() == jw.size()); } } @@ -380,8 +384,9 @@ BOOST_AUTO_TEST_CASE(pauli_build_layer_jw_isomorphism) { auto jw = build_jw_sim(obs, 3); nat.propagate(nm, pre.param_map, ng, pre.params); jw.propagate(jm, pre.param_map, jg, pre.params); - BOOST_TEST_CONTEXT("prefix k=" << k) { - BOOST_TEST(nat.size() == jw.size()); + { + INFO("prefix k=" << k); + CHECK(nat.size() == jw.size()); } } } @@ -389,7 +394,7 @@ BOOST_AUTO_TEST_CASE(pauli_build_layer_jw_isomorphism) { // On a native Pauli circuit that includes a single-qubit X layer (odd-popcount generator), the fused // propagate path, the graph replay (which recomputes the cos from the fold) and the JW-Majorana // reference must all agree. -BOOST_AUTO_TEST_CASE(pauli_build_layer_replay_fold_consumers) { +TEST_CASE("pauli_build_layer_replay_fold_consumers") { constexpr size_t N = 3; const std::map obs{{"ZII", 0.5}, {"IZI", -0.3}, {"YIY", 0.4}, {"XZI", 0.2}, {"IIZ", 0.6}}; const VecZ initial_state{0}; @@ -400,7 +405,7 @@ BOOST_AUTO_TEST_CASE(pauli_build_layer_replay_fold_consumers) { auto mp = build_pauli_sim(obs, 3); mp.build_graph({slots_of_string("XII")}, VecZ{0}, VecD{1.0}); const auto layers = mp.graph_data(); - BOOST_TEST_REQUIRE(layers.size() == 1U); + REQUIRE(layers.size() == 1U); const VecZ &cos_inds = std::get<0>(layers[0]); std::set got(cos_inds.begin(), cos_inds.end()); const auto Gb = indices_to_bitset(slots_of_string("XII")); @@ -411,7 +416,7 @@ BOOST_AUTO_TEST_CASE(pauli_build_layer_replay_fold_consumers) { expected.insert(idx); } }); - BOOST_TEST((got == expected)); + CHECK((got == expected)); } PauliCircuit circ; @@ -448,7 +453,7 @@ BOOST_AUTO_TEST_CASE(pauli_build_layer_replay_fold_consumers) { jw.build_graph(jw_majs, circ.param_map, jw_gcs); const double e_jw = jw.expectation_value(circ.params); - BOOST_TEST(e_prop == e_graph, boost::test_tools::tolerance(1e-9)); - BOOST_TEST(e_contract == e_graph, boost::test_tools::tolerance(1e-9)); - BOOST_TEST(e_jw == e_graph, boost::test_tools::tolerance(1e-9)); + CHECK((e_prop) == Catch::Approx(e_graph).epsilon(1e-9)); + CHECK((e_contract) == Catch::Approx(e_graph).epsilon(1e-9)); + CHECK((e_jw) == Catch::Approx(e_graph).epsilon(1e-9)); } diff --git a/cpp/tests/row_accessor_tests.cpp b/cpp/tests/row_accessor_tests.cpp index 75ce9afb..3bb8a3a9 100644 --- a/cpp/tests/row_accessor_tests.cpp +++ b/cpp/tests/row_accessor_tests.cpp @@ -14,7 +14,9 @@ // The dense-vector and packed OperatorIndex backends must agree through every TypeAliases.h accessor. -#include +#include +#include +#include #include @@ -45,26 +47,26 @@ auto check_backends_agree(const std::vector> &raw_rows) -> v packed.push_back(m); } - BOOST_REQUIRE(packed.size() == dense.size()); + REQUIRE(packed.size() == dense.size()); for (size_t i = 0; i < dense.size(); ++i) { - BOOST_TEST((materialize_row(dense, i) == materialize_row(packed, i))); - BOOST_TEST(row_popcount(dense, i) == row_popcount(packed, i)); - BOOST_TEST(row_popcount(dense, i) == materialize_row(dense, i).count()); - BOOST_TEST(positions_of(dense, i) == positions_of(packed, i)); + CHECK((materialize_row(dense, i) == materialize_row(packed, i))); + CHECK(row_popcount(dense, i) == row_popcount(packed, i)); + CHECK(row_popcount(dense, i) == materialize_row(dense, i).count()); + CHECK(positions_of(dense, i) == positions_of(packed, i)); } } } // namespace -BOOST_AUTO_TEST_CASE(row_accessor_backends_agree_single_word) { +TEST_CASE("row_accessor_backends_agree_single_word") { check_backends_agree<32>({{0, 3, 5}, {1, 2}, {}, {63}, {0, 1, 2, 3, 62, 63}}); } -BOOST_AUTO_TEST_CASE(row_accessor_backends_agree_multi_word) { +TEST_CASE("row_accessor_backends_agree_multi_word") { check_backends_agree<96>({{0, 64, 191}, {5, 63, 64, 65}, {}, {128, 190}}); } -BOOST_AUTO_TEST_CASE(row_accessor_assign_row_overwrites) { +TEST_CASE("row_accessor_assign_row_overwrites") { constexpr size_t N = 32; std::vector> dense; detail::OperatorIndex packed; @@ -81,8 +83,8 @@ BOOST_AUTO_TEST_CASE(row_accessor_assign_row_overwrites) { assign_row(dense, 0, replacement); assign_row(packed, 0, replacement); - BOOST_TEST((materialize_row(dense, 0) == replacement)); - BOOST_TEST((materialize_row(packed, 0) == replacement)); - BOOST_TEST(row_popcount(packed, 0) == 3U); - BOOST_TEST(positions_of(dense, 0) == positions_of(packed, 0)); + CHECK((materialize_row(dense, 0) == replacement)); + CHECK((materialize_row(packed, 0) == replacement)); + CHECK(row_popcount(packed, 0) == 3U); + CHECK(positions_of(dense, 0) == positions_of(packed, 0)); } diff --git a/cpp/tests/shm_comm_tests.cpp b/cpp/tests/shm_comm_tests.cpp index d15108d8..b275f11e 100644 --- a/cpp/tests/shm_comm_tests.cpp +++ b/cpp/tests/shm_comm_tests.cpp @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include +#include +#include +#include #include #include @@ -42,7 +44,7 @@ auto run_shm(int s, Body body) -> std::vector { } // namespace // alltoall_counts is a transpose: recv[s] on rank r == what s declared it sends to r. -BOOST_AUTO_TEST_CASE(shm_comm_alltoall_counts_transpose) { +TEST_CASE("shm_comm_alltoall_counts_transpose") { for (const int S : {2, 4, 8}) { std::vector> recv(static_cast(S)); auto errs = run_shm(S, [&](ShmComm &sh, int r) { @@ -55,11 +57,11 @@ BOOST_AUTO_TEST_CASE(shm_comm_alltoall_counts_transpose) { recv[static_cast(r)] = got; }); for (const auto &e : errs) { - BOOST_CHECK(e == nullptr); + CHECK(e == nullptr); } for (int r = 0; r < S; ++r) { for (int s = 0; s < S; ++s) { - BOOST_CHECK_EQUAL(recv[static_cast(r)][static_cast(s)], s * 100 + r); + CHECK((recv[static_cast(r)][static_cast(s)]) == (s * 100 + r)); } } } @@ -67,7 +69,7 @@ BOOST_AUTO_TEST_CASE(shm_comm_alltoall_counts_transpose) { // begin_alltoallv (the vector-of-vectors facade) must deliver each source's block contiguously in // ascending source order — what Resolve.h's positional pairing depends on. Rank r sends (r+1) tagged elts. -BOOST_AUTO_TEST_CASE(shm_comm_begin_alltoallv_source_order_and_tags) { +TEST_CASE("shm_comm_begin_alltoallv_source_order_and_tags") { for (const int S : {2, 4, 8}) { std::vector>> recv(static_cast(S)); auto errs = run_shm(S, [&](ShmComm &sh, int r) { @@ -84,16 +86,16 @@ BOOST_AUTO_TEST_CASE(shm_comm_begin_alltoallv_source_order_and_tags) { recv[static_cast(r)] = got; }); for (const auto &e : errs) { - BOOST_CHECK(e == nullptr); + CHECK(e == nullptr); } for (int r = 0; r < S; ++r) { const auto &got = recv[static_cast(r)]; - BOOST_REQUIRE_EQUAL(static_cast(got.size()), S); + REQUIRE((static_cast(got.size())) == (S)); for (int s = 0; s < S; ++s) { const auto &blk = got[static_cast(s)]; - BOOST_REQUIRE_EQUAL(static_cast(blk.size()), s + 1); // source s sent (s+1) + REQUIRE((static_cast(blk.size())) == (s + 1)); // source s sent (s+1) for (int j = 0; j <= s; ++j) { - BOOST_CHECK_EQUAL(blk[static_cast(j)], s * 1000 + j); + CHECK((blk[static_cast(j)]) == (s * 1000 + j)); } } } @@ -101,7 +103,7 @@ BOOST_AUTO_TEST_CASE(shm_comm_begin_alltoallv_source_order_and_tags) { } // skip_self: the self slot is neither sent nor received; every other source arrives intact. -BOOST_AUTO_TEST_CASE(shm_comm_begin_alltoallv_skip_self) { +TEST_CASE("shm_comm_begin_alltoallv_skip_self") { const int S = 4; std::vector>> recv(static_cast(S)); auto errs = run_shm(S, [&](ShmComm &sh, int r) { @@ -116,24 +118,24 @@ BOOST_AUTO_TEST_CASE(shm_comm_begin_alltoallv_skip_self) { recv[static_cast(r)] = got; }); for (const auto &e : errs) { - BOOST_CHECK(e == nullptr); + CHECK(e == nullptr); } for (int r = 0; r < S; ++r) { for (int s = 0; s < S; ++s) { const auto &blk = recv[static_cast(r)][static_cast(s)]; if (s == r) { - BOOST_CHECK(blk.empty()); + CHECK(blk.empty()); } else { - BOOST_REQUIRE_EQUAL(static_cast(blk.size()), 2); - BOOST_CHECK_EQUAL(blk[0], s * 10 + 1); + REQUIRE((static_cast(blk.size())) == (2)); + CHECK((blk[0]) == (s * 10 + 1)); } } } } // allreduce_sum is bit-identical on every rank and equals the fixed-order reference. -BOOST_AUTO_TEST_CASE(shm_comm_allreduce_sum_bit_identical) { +TEST_CASE("shm_comm_allreduce_sum_bit_identical") { for (const int S : {2, 4, 8}) { std::vector int_res(static_cast(S)); std::vector dbl_res(static_cast(S)); @@ -142,21 +144,21 @@ BOOST_AUTO_TEST_CASE(shm_comm_allreduce_sum_bit_identical) { dbl_res[static_cast(r)] = sh.allreduce_sum(r, static_cast(r) + 0.5); }); for (const auto &e : errs) { - BOOST_CHECK(e == nullptr); + CHECK(e == nullptr); } const size_t expect_int = static_cast(S) * (static_cast(S) + 1) / 2; // sum 1..S const double expect_dbl = static_cast(S) * static_cast(S) / 2.0; // sum (r+0.5) for (int r = 0; r < S; ++r) { - BOOST_CHECK_EQUAL(int_res[static_cast(r)], expect_int); - BOOST_CHECK_EQUAL(dbl_res[static_cast(r)], dbl_res[0]); - BOOST_CHECK_CLOSE(dbl_res[static_cast(r)], expect_dbl, 1e-12); + CHECK((int_res[static_cast(r)]) == (expect_int)); + CHECK((dbl_res[static_cast(r)]) == (dbl_res[0])); + CHECK((dbl_res[static_cast(r)]) == Catch::Approx(expect_dbl).epsilon((1e-12) / 100.0)); } } } // allreduce_sum_inplace sums element-wise; every rank ends bit-identical to the ascending-rank-order // reference. Lengths straddle the slice edges: shorter than S (empty slices), partial lines, many lines. -BOOST_AUTO_TEST_CASE(shm_comm_allreduce_sum_inplace_vector) { +TEST_CASE("shm_comm_allreduce_sum_inplace_vector") { for (const int S : {2, 4, 8}) { for (const size_t N : {size_t{1}, size_t{5}, size_t{8 * 2 + 3}, size_t{8} * static_cast(S) + 7, size_t{257}}) { @@ -175,7 +177,7 @@ BOOST_AUTO_TEST_CASE(shm_comm_allreduce_sum_inplace_vector) { res[static_cast(r)] = v; }); for (const auto &e : errs) { - BOOST_CHECK(e == nullptr); + CHECK(e == nullptr); } std::vector ref(N); // ascending-rank-order reference over the identical stored inputs for (size_t k = 0; k < N; ++k) { @@ -186,9 +188,9 @@ BOOST_AUTO_TEST_CASE(shm_comm_allreduce_sum_inplace_vector) { ref[k] = acc; } for (int r = 0; r < S; ++r) { - BOOST_REQUIRE_EQUAL(res[static_cast(r)].size(), N); + REQUIRE((res[static_cast(r)].size()) == (N)); for (size_t k = 0; k < N; ++k) { - BOOST_CHECK_EQUAL(res[static_cast(r)][k], ref[k]); + CHECK((res[static_cast(r)][k]) == (ref[k])); } } } @@ -197,7 +199,7 @@ BOOST_AUTO_TEST_CASE(shm_comm_allreduce_sum_inplace_vector) { // post_flat_alltoallv over caller-owned flat buffers (the Evolution/Pare replay path): each rank sends // its own id, so every target must receive [0,1,..,S-1] in source order. -BOOST_AUTO_TEST_CASE(shm_comm_post_flat_alltoallv_flat_buffers) { +TEST_CASE("shm_comm_post_flat_alltoallv_flat_buffers") { const int S = 4; std::vector> recv(static_cast(S)); auto errs = run_shm(S, [&](ShmComm &sh, int r) { @@ -224,18 +226,18 @@ BOOST_AUTO_TEST_CASE(shm_comm_post_flat_alltoallv_flat_buffers) { recv[static_cast(r)] = out; }); for (const auto &e : errs) { - BOOST_CHECK(e == nullptr); + CHECK(e == nullptr); } for (int r = 0; r < S; ++r) { for (int s = 0; s < S; ++s) { - BOOST_CHECK_EQUAL(recv[static_cast(r)][static_cast(s)], s); + CHECK((recv[static_cast(r)][static_cast(s)]) == (s)); } } } // alltoallv_resolve resolves recv_counts (the transpose) AND moves the payload in one 2-sync round, // sizing recv itself — what begin_alltoallv takes for unknown-layout Shm rounds. -BOOST_AUTO_TEST_CASE(shm_comm_alltoallv_resolve_fused) { +TEST_CASE("shm_comm_alltoallv_resolve_fused") { for (const int S : {2, 4, 8}) { const int rounds = 25; std::atomic failures{0}; @@ -288,14 +290,14 @@ BOOST_AUTO_TEST_CASE(shm_comm_alltoallv_resolve_fused) { } }); for (const auto &e : errs) { - BOOST_CHECK(e == nullptr); + CHECK(e == nullptr); } - BOOST_CHECK_EQUAL(failures.load(), 0); + CHECK((failures.load()) == (0)); } } // Repeated collectives reuse one ShmComm across many rounds without drift (barrier generation reuse). -BOOST_AUTO_TEST_CASE(shm_comm_repeated_collectives) { +TEST_CASE("shm_comm_repeated_collectives") { const int S = 8; std::atomic failures{0}; auto errs = run_shm(S, [&](ShmComm &sh, int r) { @@ -307,14 +309,14 @@ BOOST_AUTO_TEST_CASE(shm_comm_repeated_collectives) { } }); for (const auto &e : errs) { - BOOST_CHECK(e == nullptr); + CHECK(e == nullptr); } - BOOST_CHECK_EQUAL(failures.load(), 0); + CHECK((failures.load()) == (0)); } // Oversubscribed: the barrier's bounded spin must fall back to yielding or spinners starve the completer // of a core. The test completing at all proves liveness. -BOOST_AUTO_TEST_CASE(shm_comm_oversubscribed_repeated_collectives) { +TEST_CASE("shm_comm_oversubscribed_repeated_collectives") { const unsigned hw = std::max(1u, std::thread::hardware_concurrency()); const int S = static_cast(std::min(64u, std::max(8u, 2 * hw))); std::atomic failures{0}; @@ -327,14 +329,14 @@ BOOST_AUTO_TEST_CASE(shm_comm_oversubscribed_repeated_collectives) { } }); for (const auto &e : errs) { - BOOST_CHECK(e == nullptr); + CHECK(e == nullptr); } - BOOST_CHECK_EQUAL(failures.load(), 0); + CHECK((failures.load()) == (0)); } // Poison: if one participant unwinds instead of arriving, peers waiting in a barrier must throw // ShmCommPoisoned rather than hang. The test completing at all proves no deadlock. -BOOST_AUTO_TEST_CASE(shm_comm_poison_releases_waiters) { +TEST_CASE("shm_comm_poison_releases_waiters") { for (const int S : {2, 4, 8}) { auto errs = run_shm(S, [&](ShmComm &sh, int r) { if (r == 0) { @@ -345,10 +347,10 @@ BOOST_AUTO_TEST_CASE(shm_comm_poison_releases_waiters) { std::vector send(static_cast(S), 1), got(static_cast(S)); sh.alltoall_counts(r, send.data(), got.data()); }); - BOOST_CHECK(errs[0] == nullptr); + CHECK(errs[0] == nullptr); for (int r = 1; r < S; ++r) { - BOOST_REQUIRE(errs[static_cast(r)] != nullptr); - BOOST_CHECK_THROW(std::rethrow_exception(errs[static_cast(r)]), ShmCommPoisoned); + REQUIRE(errs[static_cast(r)] != nullptr); + CHECK_THROWS_AS(std::rethrow_exception(errs[static_cast(r)]), ShmCommPoisoned); } } } diff --git a/cpp/tests/simulator_copy_tests.cpp b/cpp/tests/simulator_copy_tests.cpp index be79c160..b1c26b96 100644 --- a/cpp/tests/simulator_copy_tests.cpp +++ b/cpp/tests/simulator_copy_tests.cpp @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include +#include +#include +#include #include @@ -32,65 +34,65 @@ static_assert(std::is_copy_constructible_v>, "simulator mu static_assert(std::is_move_constructible_v>, "simulator must stay movable"); static_assert(!std::is_copy_assignable_v>, "copy assignment stays deleted"); -BOOST_FIXTURE_TEST_CASE(copy_constructed_simulator_matches_energy, ExampleDataFix) { +TEST_CASE_METHOD(ExampleDataFix, "copy_constructed_simulator_matches_energy") { SimulatorConfig cfg{.comm = MPI_COMM_SELF}; auto sim = build_simulator(data, cfg); sim.build_graph(data.majoranas, data.param_inds, data.gen_coeffs); auto copy = sim; - BOOST_TEST(copy.graph_layers() == sim.graph_layers()); - BOOST_TEST(copy.size() == sim.size()); + CHECK(copy.graph_layers() == sim.graph_layers()); + CHECK(copy.size() == sim.size()); const double e_orig = sim.expectation_value_functional()(data.parameters); const double e_copy = copy.expectation_value_functional()(data.parameters); - BOOST_CHECK_SMALL(e_orig - e_copy, 1e-13); + CHECK_THAT(e_orig - e_copy, Catch::Matchers::WithinAbs(0.0, 1e-13)); } -BOOST_FIXTURE_TEST_CASE(copy_is_independent_of_source, ExampleDataFix) { +TEST_CASE_METHOD(ExampleDataFix, "copy_is_independent_of_source") { SimulatorConfig cfg{.comm = MPI_COMM_SELF}; auto sim = build_simulator(data, cfg); auto copy = sim; - BOOST_TEST(copy.graph_layers() == 0u); + CHECK(copy.graph_layers() == 0u); sim.build_graph(data.majoranas, data.param_inds, data.gen_coeffs); - BOOST_TEST(sim.graph_layers() > 0u); - BOOST_TEST(copy.graph_layers() == 0u); + CHECK(sim.graph_layers() > 0u); + CHECK(copy.graph_layers() == 0u); copy.build_graph(data.majoranas, data.param_inds, data.gen_coeffs); const double e_sim = sim.expectation_value_functional()(data.parameters); const double e_copy = copy.expectation_value_functional()(data.parameters); - BOOST_CHECK_SMALL(e_sim - e_copy, 1e-13); + CHECK_THAT(e_sim - e_copy, Catch::Matchers::WithinAbs(0.0, 1e-13)); } // The layer list is per-instance (vector); the immutable LayerCores are shared via shared_ptr. // Contracting one copy in place truncates only its own layer list, and destroying it only drops its // core references. -BOOST_FIXTURE_TEST_CASE(copy_graph_survives_other_being_contracted_and_destroyed, ExampleDataFix) { +TEST_CASE_METHOD(ExampleDataFix, "copy_graph_survives_other_being_contracted_and_destroyed") { SimulatorConfig cfg{.comm = MPI_COMM_SELF}; auto original = build_simulator(data, cfg); original.build_graph(data.majoranas, data.param_inds, data.gen_coeffs); const size_t layers_before = original.graph_layers(); - BOOST_TEST(layers_before > 0u); + CHECK(layers_before > 0u); const double e_before = original.expectation_value_functional()(data.parameters); { auto copy = original; - BOOST_TEST(copy.graph_layers() == layers_before); + CHECK(copy.graph_layers() == layers_before); copy.contract_partially(data.parameters, /*inplace=*/true); - BOOST_TEST(copy.graph_layers() < layers_before); - BOOST_TEST(original.graph_layers() == layers_before); + CHECK(copy.graph_layers() < layers_before); + CHECK(original.graph_layers() == layers_before); } - BOOST_TEST(original.graph_layers() == layers_before); + CHECK(original.graph_layers() == layers_before); const double e_after = original.expectation_value_functional()(data.parameters); - BOOST_CHECK_SMALL(e_before - e_after, 1e-13); + CHECK_THAT(e_before - e_after, Catch::Matchers::WithinAbs(0.0, 1e-13)); } -BOOST_FIXTURE_TEST_CASE(copy_constructed_simulator_index_valid, ExampleDataFix) { +TEST_CASE_METHOD(ExampleDataFix, "copy_constructed_simulator_index_valid") { SimulatorConfig cfg{.comm = MPI_COMM_SELF}; auto sim = build_simulator(data, cfg); sim.build_graph(data.majoranas, data.param_inds, data.gen_coeffs); @@ -98,7 +100,7 @@ BOOST_FIXTURE_TEST_CASE(copy_constructed_simulator_index_valid, ExampleDataFix) auto copy = sim; const auto &idx = copy.indexing(); - BOOST_TEST(idx.size() == sim.indexing().size()); + CHECK(idx.size() == sim.indexing().size()); bool all_found = true; idx.for_each([&](const auto &mono, size_t i) { const auto f = idx.find(mono); @@ -106,5 +108,5 @@ BOOST_FIXTURE_TEST_CASE(copy_constructed_simulator_index_valid, ExampleDataFix) all_found = false; } }); - BOOST_TEST(all_found); + CHECK(all_found); } diff --git a/cpp/tests/unit_tests.cpp b/cpp/tests/unit_tests.cpp index 06ceb530..879c889e 100644 --- a/cpp/tests/unit_tests.cpp +++ b/cpp/tests/unit_tests.cpp @@ -12,23 +12,25 @@ // See the License for the specific language governing permissions and // limitations under the License. -#define BOOST_TEST_MODULE "MonoProp Unit Tests" - #include -#include +#include #include "monoprop/detail/mpi/MPICompat.h" -static auto init() -> bool { - return true; -} - auto main(int argc, char* argv[]) -> int { // overwrite=0, so an explicit environment override still wins; why it is off: tests/cpp/README.md. setenv("monoprop_PARTITIONS", "off", 0); monoprop::mpi::init(&argc, &argv); - int result = boost::unit_test::unit_test_main(&init, argc, argv); + + Catch::Session session; + const int command_line_result = session.applyCommandLine(argc, argv); + if (command_line_result != 0) { + monoprop::mpi::finalize(); + return command_line_result; + } + + const int result = session.run(); monoprop::mpi::finalize(); return result; } diff --git a/cpp/tests/update_initial_operator.cpp b/cpp/tests/update_initial_operator.cpp index 76dc74ac..d9fa39cd 100644 --- a/cpp/tests/update_initial_operator.cpp +++ b/cpp/tests/update_initial_operator.cpp @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include +#include +#include +#include #include #include @@ -23,10 +25,7 @@ #include "monoprop/detail/mpi/MPICompat.h" using namespace monoprop; -namespace tt = boost::test_tools; -namespace utf = boost::unit_test; - -BOOST_AUTO_TEST_CASE(update_initial_operator_updates_core_expval) { +TEST_CASE("update_initial_operator_updates_core_expval") { constexpr size_t n_modes = 2; OperatorDict initial_ham; initial_ham[VecZ{}] = std::complex{1.0, 0.0}; @@ -44,21 +43,21 @@ BOOST_AUTO_TEST_CASE(update_initial_operator_updates_core_expval) { const VecD empty_params; auto expval_fn = simulator.expectation_value_functional(std::nullopt); - BOOST_TEST(expval_fn(empty_params) == 1.0, tt::tolerance(1e-12)); + CHECK((expval_fn(empty_params)) == Catch::Approx(1.0).epsilon(1e-12)); OperatorDict updated; updated[VecZ{}] = std::complex{2.75, 0.0}; simulator.update_initial_operator(updated); auto updated_fn = simulator.expectation_value_functional(std::nullopt); - BOOST_TEST(updated_fn(empty_params) == 2.75, tt::tolerance(1e-12)); + CHECK((updated_fn(empty_params)) == Catch::Approx(2.75).epsilon(1e-12)); // The functional built before the re-weight snapshotted the old coefficients, so it must reject // the call rather than answer for an operator the propagator no longer holds. - BOOST_CHECK_THROW(expval_fn(empty_params), std::runtime_error); + CHECK_THROWS_AS(expval_fn(empty_params), std::runtime_error); } -BOOST_AUTO_TEST_CASE(update_initial_operator_invalidates_gradient_functional) { +TEST_CASE("update_initial_operator_invalidates_gradient_functional") { constexpr size_t n_modes = 2; OperatorDict initial_ham; initial_ham[VecZ{}] = std::complex{1.0, 0.0}; @@ -76,18 +75,18 @@ BOOST_AUTO_TEST_CASE(update_initial_operator_invalidates_gradient_functional) { const VecD empty_params; auto grad_fn = simulator.expectation_value_and_gradient_functional(std::nullopt); - BOOST_TEST(grad_fn(empty_params).first == 1.0, tt::tolerance(1e-12)); + CHECK((grad_fn(empty_params).first) == Catch::Approx(1.0).epsilon(1e-12)); OperatorDict updated; updated[VecZ{}] = std::complex{2.75, 0.0}; simulator.update_initial_operator(updated); - BOOST_CHECK_THROW(grad_fn(empty_params), std::runtime_error); - BOOST_TEST(simulator.expectation_value_and_gradient_functional(std::nullopt)(empty_params).first == 2.75, - tt::tolerance(1e-12)); + CHECK_THROWS_AS(grad_fn(empty_params), std::runtime_error); + CHECK((simulator.expectation_value_and_gradient_functional(std::nullopt)(empty_params).first) + == Catch::Approx(2.75).epsilon(1e-12)); } -BOOST_AUTO_TEST_CASE(update_initial_operator_throws_for_unknown_term_in_heisenberg) { +TEST_CASE("update_initial_operator_throws_for_unknown_term_in_heisenberg") { constexpr size_t n_modes = 2; OperatorDict initial_ham; initial_ham[VecZ{0, 1}] = std::complex{0, 1.0}; @@ -108,10 +107,10 @@ BOOST_AUTO_TEST_CASE(update_initial_operator_throws_for_unknown_term_in_heisenbe missing_term[invalid_term] = std::complex{0.0, 0.5}; // On a single rank, the owning rank always sees the error. - BOOST_CHECK_THROW(simulator.update_initial_operator(missing_term), std::runtime_error); + CHECK_THROWS_AS(simulator.update_initial_operator(missing_term), std::runtime_error); } -BOOST_AUTO_TEST_CASE(update_initial_operator_accepts_new_terms_in_schrodinger) { +TEST_CASE("update_initial_operator_accepts_new_terms_in_schrodinger") { constexpr size_t n_modes = 2; OperatorDict initial_ham; initial_ham[VecZ{0, 1}] = std::complex{0, 1.0}; @@ -130,5 +129,5 @@ BOOST_AUTO_TEST_CASE(update_initial_operator_accepts_new_terms_in_schrodinger) { OperatorDict new_term; new_term[VecZ{2, 3}] = std::complex{0.0, 0.25}; - BOOST_CHECK_NO_THROW(simulator.update_initial_operator(new_term)); + CHECK_NOTHROW(simulator.update_initial_operator(new_term)); } diff --git a/cpp/tests/validation_tests.cpp b/cpp/tests/validation_tests.cpp index 173c3a2d..bd6bfd8d 100644 --- a/cpp/tests/validation_tests.cpp +++ b/cpp/tests/validation_tests.cpp @@ -14,7 +14,9 @@ // The validators in Validation.cpp that guard the public build/propagate/functional API. -#include +#include +#include +#include #include @@ -23,47 +25,47 @@ using namespace monoprop; -BOOST_AUTO_TEST_CASE(validation_coefficient_lengths) { - BOOST_CHECK_NO_THROW(validate_coefficient_lengths(VecZ{0, 1, 2}, VecD{1.0, 2.0, 3.0})); - BOOST_CHECK_NO_THROW(validate_coefficient_lengths(VecZ{}, VecD{})); - BOOST_CHECK_THROW(validate_coefficient_lengths(VecZ{0, 1}, VecD{1.0}), std::runtime_error); +TEST_CASE("validation_coefficient_lengths") { + CHECK_NOTHROW(validate_coefficient_lengths(VecZ{0, 1, 2}, VecD{1.0, 2.0, 3.0})); + CHECK_NOTHROW(validate_coefficient_lengths(VecZ{}, VecD{})); + CHECK_THROWS_AS(validate_coefficient_lengths(VecZ{0, 1}, VecD{1.0}), std::runtime_error); } -BOOST_AUTO_TEST_CASE(validation_gate_indices) { - BOOST_CHECK_NO_THROW(validate_gate_indices(VecZ{0, 0, 1, 1, 2}, 5)); - BOOST_CHECK_NO_THROW(validate_gate_indices(VecZ{}, 0)); - BOOST_CHECK_NO_THROW(validate_gate_indices(VecZ{0, 1, 2}, 3)); +TEST_CASE("validation_gate_indices") { + CHECK_NOTHROW(validate_gate_indices(VecZ{0, 0, 1, 1, 2}, 5)); + CHECK_NOTHROW(validate_gate_indices(VecZ{}, 0)); + CHECK_NOTHROW(validate_gate_indices(VecZ{0, 1, 2}, 3)); // Length must match the monomial count. - BOOST_CHECK_THROW(validate_gate_indices(VecZ{0, 1}, 3), std::runtime_error); + CHECK_THROWS_AS(validate_gate_indices(VecZ{0, 1}, 3), std::runtime_error); // Must start at 0. - BOOST_CHECK_THROW(validate_gate_indices(VecZ{1, 2}, 2), std::runtime_error); + CHECK_THROWS_AS(validate_gate_indices(VecZ{1, 2}, 2), std::runtime_error); // Must not jump by more than 1. - BOOST_CHECK_THROW(validate_gate_indices(VecZ{0, 1, 3}, 3), std::runtime_error); + CHECK_THROWS_AS(validate_gate_indices(VecZ{0, 1, 3}, 3), std::runtime_error); // Must not decrease. - BOOST_CHECK_THROW(validate_gate_indices(VecZ{0, 1, 0}, 3), std::runtime_error); + CHECK_THROWS_AS(validate_gate_indices(VecZ{0, 1, 0}, 3), std::runtime_error); } -BOOST_AUTO_TEST_CASE(validation_parameters_length) { - BOOST_CHECK_NO_THROW(validate_parameters_length(VecD{0.1, 0.2, 0.3}, VecZ{0, 1, 2})); - BOOST_CHECK_NO_THROW(validate_parameters_length(VecD{0.1, 0.2}, VecZ{0, 1, 1, 0})); // max=1 -> len 2 - BOOST_CHECK_NO_THROW(validate_parameters_length(VecD{}, VecZ{})); - BOOST_CHECK_THROW(validate_parameters_length(VecD{0.1, 0.2}, VecZ{0, 1, 2}), std::runtime_error); +TEST_CASE("validation_parameters_length") { + CHECK_NOTHROW(validate_parameters_length(VecD{0.1, 0.2, 0.3}, VecZ{0, 1, 2})); + CHECK_NOTHROW(validate_parameters_length(VecD{0.1, 0.2}, VecZ{0, 1, 1, 0})); // max=1 -> len 2 + CHECK_NOTHROW(validate_parameters_length(VecD{}, VecZ{})); + CHECK_THROWS_AS(validate_parameters_length(VecD{0.1, 0.2}, VecZ{0, 1, 2}), std::runtime_error); } -BOOST_AUTO_TEST_CASE(validation_functional_call) { - BOOST_CHECK_NO_THROW(validate_functional_call(VecD{0.1, 0.2}, 2)); - BOOST_CHECK_NO_THROW(validate_functional_call(VecD{}, 0)); - BOOST_CHECK_THROW(validate_functional_call(VecD{0.1}, 2), std::runtime_error); +TEST_CASE("validation_functional_call") { + CHECK_NOTHROW(validate_functional_call(VecD{0.1, 0.2}, 2)); + CHECK_NOTHROW(validate_functional_call(VecD{}, 0)); + CHECK_THROWS_AS(validate_functional_call(VecD{0.1}, 2), std::runtime_error); } -BOOST_AUTO_TEST_CASE(validation_expected_graph_layers) { - BOOST_CHECK_NO_THROW(validate_expected_graph_layers(3, 3)); - BOOST_CHECK_THROW(validate_expected_graph_layers(4, 3), std::runtime_error); +TEST_CASE("validation_expected_graph_layers") { + CHECK_NOTHROW(validate_expected_graph_layers(3, 3)); + CHECK_THROWS_AS(validate_expected_graph_layers(4, 3), std::runtime_error); } -BOOST_AUTO_TEST_CASE(validation_only_rotate_len_k) { - BOOST_CHECK_NO_THROW(validate_only_rotate_len_k_(std::nullopt, 8)); - BOOST_CHECK_NO_THROW(validate_only_rotate_len_k_(8u, 8)); - BOOST_CHECK_THROW(validate_only_rotate_len_k_(0u, 8), std::runtime_error); - BOOST_CHECK_THROW(validate_only_rotate_len_k_(9u, 8), std::runtime_error); +TEST_CASE("validation_only_rotate_len_k") { + CHECK_NOTHROW(validate_only_rotate_len_k_(std::nullopt, 8)); + CHECK_NOTHROW(validate_only_rotate_len_k_(8u, 8)); + CHECK_THROWS_AS(validate_only_rotate_len_k_(0u, 8), std::runtime_error); + CHECK_THROWS_AS(validate_only_rotate_len_k_(9u, 8), std::runtime_error); } diff --git a/docs/content/docs/building.mdx b/docs/content/docs/building.mdx index aa0fe949..830e5aca 100644 --- a/docs/content/docs/building.mdx +++ b/docs/content/docs/building.mdx @@ -101,7 +101,8 @@ ctest --test-dir build/editable/Release ``` This uses the scikit-build-core Release tree at `build/editable/Release` and -runs `bin/monoprop_unit_tests.x` there. +runs `bin/monoprop_unit_tests.x` there. The test sources compile at `-O1` to +reduce template-heavy build time while the library remains at Release `-O3`. ### Debug tree diff --git a/docs/content/docs/testing.mdx b/docs/content/docs/testing.mdx index 51326cff..cb6b5f1a 100644 --- a/docs/content/docs/testing.mdx +++ b/docs/content/docs/testing.mdx @@ -61,6 +61,9 @@ uv sync --all-extras ctest --test-dir build/editable/Release --output-on-failure ``` +Release builds use `-O1` for the test sources while retaining `-O3` for the +library. + With MPI, reuse the MPI-enabled `uv sync` from the Python MPI section above, then run CTest against the same tree: @@ -85,13 +88,13 @@ A 64-bit `TermIndex` build is the only configuration that compiles the just test-wide # rebuilds via uv sync with monoprop_WIDE_TERM_INDEX=ON, then runs CTest ``` -CTest runs each Boost case as its own process, so an MPI build's `MPI_Init` probes every +CTest runs each Catch2 case as its own process, so an MPI build's `MPI_Init` probes every fabric device per case, whether or not the test sends anything. `monoprop_TEST_EXCLUDE_MPI_FABRIC=ON` (default) skips that probe for the single-process `serial` variants only; multi-rank variants keep the full component set, since they exchange real messages. Turn it off by adding `-Dmonoprop_TEST_EXCLUDE_MPI_FABRIC=OFF` to the `SKBUILD_CMAKE_ARGS` MPI build above. -CTest registers every Boost case individually as a `serial` variant. When the +CTest registers every Catch2 case individually as a `serial` variant. When the build has MPI enabled and a launcher is found, it also registers the whole suite once per rank count in `monoprop_MPI_TEST_PROCS`, labelled `mpi` and `mpi-` — one entry per rank count rather than per case, because the ranks have to reach @@ -181,39 +184,36 @@ class CasesFermionicProblem: ### C++ tests -C++ tests live in `cpp/tests/` and use [Boost.Test](https://www.boost.org/doc/libs/release/libs/test/). They are registered automatically via CMake: new `*.cpp` files in `cpp/tests/` are picked up on the next configure, so no source-list edit is needed. +C++ tests live in `cpp/tests/` and use [Catch2 v3](https://github.com/catchorg/Catch2). They are registered automatically via CMake: new `*.cpp` files in `cpp/tests/` are picked up on the next configure, so no source-list edit is needed. #### Simple unit test ```cpp -#include +#include -BOOST_AUTO_TEST_CASE(my_basic_check) { +TEST_CASE("my_basic_check") { int result = 2 + 2; - BOOST_TEST(result == 4); + CHECK(result == 4); } ``` #### Data-driven test using reference fixtures -Use the `ExampleDataFix` fixture class from `TestUtilities.h` to load the same msgpack data as Python tests, and `BOOST_DATA_TEST_CASE_F` to parametrize over it: +Use the `ExampleDataFix` fixture class from `TestUtilities.h` to load the same msgpack data as Python tests, and Catch2 generators to parametrize over it: ```cpp -#include -#include -#include +#include +#include #include "TestUtilities.h" using namespace test_utils; -namespace utf = boost::unit_test; -namespace bdata = utf::data; - -BOOST_DATA_TEST_CASE_F(ExampleDataFix, - my_new_test, - bdata::make(ds_pare_values) ^ bdata::make(ds_schrodinger_enabled), - pare, - sch_enabled) { + +TEST_CASE_METHOD(ExampleDataFix, "my_new_test") { + const auto index = GENERATE(0U, 1U); + const auto pare = ds_pare_values[index]; + const auto sch_enabled = ds_schrodinger_enabled[index]; + CAPTURE(pare, sch_enabled); const auto schrodinger_cutoff = make_schrodinger_cutoff(sch_enabled, cutoff); SimulatorConfig cfg{ .schrodinger_cutoff = schrodinger_cutoff diff --git a/pyproject.toml b/pyproject.toml index e8e1da5b..79a59272 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -386,7 +386,7 @@ test-groups = ["test"] test-extras = ["qiskit"] [tool.cibuildwheel.linux] -before-all = ["./tools/install-deps.sh --skip-boost-test --skip-msgpack"] +before-all = ["./tools/install-deps.sh --skip-catch2 --skip-msgpack"] environment = { SKBUILD_CMAKE_ARGS = "-Dmonoprop_ENABLE_ARCH_FLAGS=OFF;-Dmonoprop_ENABLE_CXX_UNIT_TESTS=OFF;-Dmonoprop_ENABLE_MPI=OFF" } [tool.cibuildwheel.macos] diff --git a/tools/install-deps.sh b/tools/install-deps.sh index 027c6d01..dfba5df5 100755 --- a/tools/install-deps.sh +++ b/tools/install-deps.sh @@ -10,7 +10,7 @@ Usage: $0 [INSTALL_PREFIX] [OPTIONS] Install C++ dependencies for monoprop project. -This script can install Boost Unordered, Boost Test, msgpack-cxx, and hwloc. +This script can install Boost Unordered, Catch2, msgpack-cxx, and hwloc. Each component can be skipped with the corresponding option. The default installation prefix is /usr/local. @@ -19,7 +19,7 @@ Arguments: Options: --skip-boost-unordered Skip installing Boost unordered - --skip-boost-test Skip installing Boost Test library (only install unordered) + --skip-catch2 Skip installing Catch2 --skip-msgpack Skip installing msgpack-cxx library --skip-hwloc Skip installing hwloc library --help, -h Show this help message @@ -27,9 +27,8 @@ Options: Examples: $0 # Install all deps to /usr/local $0 \$HOME/Software # Install all deps to \$HOME/Software - $0 --skip-boost-test # Skip Boost Test, install rest to default location $0 /opt --skip-msgpack # Install to /opt, skip msgpack - $0 --skip-boost-test --skip-msgpack # Minimal install (Boost unordered + hwloc) + $0 --skip-catch2 --skip-msgpack # Install Boost unordered and hwloc only EOF } @@ -37,7 +36,7 @@ EOF DEFAULT_PREFIX="/usr/local" INSTALL_PREFIX="$DEFAULT_PREFIX" INSTALL_BOOST_UNORDERED=true -INSTALL_BOOST_TEST=true +INSTALL_CATCH2=true INSTALL_MSGPACK=true INSTALL_HWLOC=true @@ -48,8 +47,8 @@ while [[ $# -gt 0 ]]; do INSTALL_BOOST_UNORDERED=false shift ;; - --skip-boost-test) - INSTALL_BOOST_TEST=false + --skip-catch2) + INSTALL_CATCH2=false shift ;; --skip-msgpack) @@ -85,7 +84,7 @@ done echo "Installing C++ dependencies to: $INSTALL_PREFIX" echo "Boost unordered: $([ "$INSTALL_BOOST_UNORDERED" = true ] && echo "YES" || echo "SKIP")" -echo "Boost Test: $([ "$INSTALL_BOOST_TEST" = true ] && echo "YES" || echo "SKIP")" +echo "Catch2: $([ "$INSTALL_CATCH2" = true ] && echo "YES" || echo "SKIP")" echo "msgpack-cxx: $([ "$INSTALL_MSGPACK" = true ] && echo "YES" || echo "SKIP")" echo "hwloc: $([ "$INSTALL_HWLOC" = true ] && echo "YES" || echo "SKIP")" echo @@ -102,7 +101,7 @@ cleanup_build() { } install_boost() { - if [ "$INSTALL_BOOST_UNORDERED" != true ] && [ "$INSTALL_BOOST_TEST" != true ]; then + if [ "$INSTALL_BOOST_UNORDERED" != true ]; then echo "Skipping Boost installation" return 0 fi @@ -120,14 +119,6 @@ install_boost() { echo " - Skipping Boost unordered library" fi - if [ "$INSTALL_BOOST_TEST" = true ]; then - echo " - Including Boost Test library" - git submodule update --depth 1 -q --init libs/test - python3 tools/boostdep/depinst/depinst.py -X test -g "--depth 1" test - else - echo " - Skipping Boost Test library" - fi - cmake -S. -Bbuild -DBUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="$INSTALL_PREFIX" cmake --build build --target install --parallel cleanup_build boost_src @@ -136,6 +127,28 @@ install_boost() { fi } +install_catch2() { + if [ "$INSTALL_CATCH2" != true ]; then + echo "Skipping Catch2 installation" + return 0 + fi + + local catch2_version="3.15.3" + echo "Installing Catch2 $catch2_version..." + git clone https://github.com/catchorg/Catch2.git -b "v$catch2_version" catch2_src --depth 1 + cd catch2_src + cmake -S. -Bbuild \ + -DCMAKE_BUILD_TYPE=Release \ + -DCATCH_BUILD_TESTING=OFF \ + -DCATCH_INSTALL_DOCS=OFF \ + -DCMAKE_INSTALL_PREFIX="$INSTALL_PREFIX" + cmake --build build --target install --parallel + cleanup_build catch2_src + if [[ "$INSTALL_PREFIX" == "$DEFAULT_PREFIX" ]]; then + echo "Remember to export Catch2_DIR=$INSTALL_PREFIX/lib/cmake/Catch2" + fi +} + install_msgpack() { if [ "$INSTALL_MSGPACK" != true ]; then echo "Skipping msgpack-cxx installation" @@ -207,6 +220,8 @@ fi install_boost +install_catch2 + install_msgpack install_hwloc @@ -220,6 +235,6 @@ echo "Make sure to set CMAKE_PREFIX_PATH=$INSTALL_PREFIX when building monoprop" echo echo "Installed components:" [ "$INSTALL_BOOST_UNORDERED" = true ] && echo " ✓ Boost unordered" || echo " ✗ Boost unordered (skipped)" -[ "$INSTALL_BOOST_TEST" = true ] && echo " ✓ Boost Test" || echo " ✗ Boost Test (skipped)" +[ "$INSTALL_CATCH2" = true ] && echo " ✓ Catch2" || echo " ✗ Catch2 (skipped)" [ "$INSTALL_MSGPACK" = true ] && echo " ✓ msgpack-cxx" || echo " ✗ msgpack-cxx (skipped)" [ "$INSTALL_HWLOC" = true ] && echo " ✓ hwloc" || echo " ✗ hwloc (skipped)"