Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
dfe5d81
test(cpp): ✅ register every case and run the MPI suite with splitmix …
robertodr Oct 28, 2026
b1dcfde
test(cpp): ✅ skip cases that need several ranks instead of passing them
robertodr Oct 28, 2026
538dab1
test(cpp): 🐛 compare serial and distributed runs on references trunca…
robertodr Oct 28, 2026
94fef2a
test(cpp): ✅ check that linear routing refuses a three-rank communicator
robertodr Oct 28, 2026
fb8de41
test(py): ♻️ run serial tests with comm=None and distribute through a…
robertodr Oct 28, 2026
5e4af55
test(py): 🥅 report failures from every MPI rank, not only rank 0
robertodr Oct 28, 2026
cc2449c
test(py): ✅ compare distributed Majorana and Pauli runs with the seri…
robertodr Oct 28, 2026
92958f3
build(just): 🔧 run the Python MPI legs in every routing and partition…
robertodr Oct 28, 2026
7b1c649
ci(test): 👷 run the MPI legs through the recipes, with a job timeout
robertodr Oct 28, 2026
05cc05a
docs(mpi): 📝 document comm=None and the MPI test configurations
robertodr Oct 28, 2026
9f232b9
docs(mpi): 📝 tighten the MPI test documentation
robertodr Oct 28, 2026
bd77372
docs(mpi): 📝 correct the splitmix rank-list advice and the three-rank…
robertodr Oct 28, 2026
1579cc3
Apply suggestion from @robertodr
robertodr Oct 28, 2026
a733f9b
test(ci): 🐛 pin the routing of linear MPI variants and report results…
robertodr Oct 30, 2026
335faa3
test(mpi): 🐛 run each Python MPI leg in exactly its own configuration…
robertodr Oct 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ jobs:
mpi: "on"

runs-on: ${{ matrix.runner}}
timeout-minutes: 60

env:
monoprop_ENABLE_MPI: "${{ matrix.mpi }}"
Expand Down Expand Up @@ -114,13 +115,11 @@ jobs:

- name: Run Python tests under MPI
if: matrix.mpi == 'on'
run: just test-py-mpi 2

- name: Run Python MPI marker rank matrix
if: matrix.mpi == 'on'
run: just test-py-mpi "1;2;4" -m mpi
run: just test-py-mpi "${{ steps.test-group-label.outputs.label }}"

# !cancelled(): a failing test step must still report the results it wrote.
- name: Upload serial test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/codecov-action@v7.1.1
with:
token: ${{ secrets.CODECOV_TOKEN }}
Expand All @@ -129,12 +128,18 @@ jobs:
disable_search: true
fail_ci_if_error: true

- name: Upload MPI C++ test results to Codecov
if: matrix.mpi == 'on'
- name: Upload MPI test results to Codecov
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
if: ${{ !cancelled() && matrix.mpi == 'on' }}
uses: codecov/codecov-action@v7.1.1
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ctest-mpi-${{ steps.test-group-label.outputs.label }}.xml
files: >-
ctest-mpi-${{ steps.test-group-label.outputs.label }}.xml,
pytest-mpi-${{ steps.test-group-label.outputs.label }}-ranks1.xml,
pytest-mpi-${{ steps.test-group-label.outputs.label }}-ranks2.xml,
pytest-mpi-${{ steps.test-group-label.outputs.label }}-ranks4.xml,
pytest-mpi-${{ steps.test-group-label.outputs.label }}-ranks3-splitmix.xml,
pytest-mpi-${{ steps.test-group-label.outputs.label }}-ranks2-partitions2.xml
report_type: test_results
disable_search: true
fail_ci_if_error: true
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ just test-mpi # Python + C++ tests under MPI
```

See the [testing guide](https://docs.monoprop.algorithmiq.tech/testing)
for the with/without-MPI details and the rank matrix. CI has explicit MPI-enabled
for the with/without-MPI details and the MPI configurations. CI has explicit MPI-enabled
lanes on Linux x86-64, Linux ARM64, and macOS; installing the `mpi` extra alone
does not enable the C++ MPI build. CI requires a registered MPI CTest variant,
and each whole-suite MPI run has a 600-second deadlock timeout. Source builds
Expand Down
25 changes: 16 additions & 9 deletions cpp/tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ discovers every Boost.Test case and registers it twice:

- **`serial`**: the case run in-process with `MPI_COMM_SELF`.
- **`mpi`** (+ rank-specific `mpi-<n>`): the whole suite wrapped in
`mpiexec -n <n>` for each rank in `monoprop_MPI_TEST_PROCS` (default `2;4`),
`mpiexec -n <n>` for each rank in `monoprop_MPI_TEST_PROCS` (default `2;4`,
also labelled `mpi-linear`), plus splitmix-routing entries for each rank in
`monoprop_MPI_SPLITMIX_TEST_PROCS` (default `2;3;4`, also labelled
`mpi-splitmix`; a CMake cache variable only, set through `SKBUILD_CMAKE_DEFINE`),
registered when an MPI launcher is detected.

Cases that need multiple ranks check `monoprop::mpi::size(MPI_COMM_WORLD)` and
skip (with a message) when run with too few.
Cases that need a particular world declare it with a precondition from
`RankPreconditions.h`; Boost reports them as skipped elsewhere, and CTest marks
those serial entries as skipped.

The custom `main()` in `unit_tests.cpp` initializes MPI and forces
`monoprop_PARTITIONS=off`, so white-box tests observe the single-partition engine.
Expand Down Expand Up @@ -77,6 +81,8 @@ name and cannot address suite-nested cases, tests use flat
- **`dense_query_reference.h`**: the retired dense query record, frozen as the
independent oracle for `sparse_query_tests.cpp`. Test-only, and not kept in
sync with the wire format.
- **`RankPreconditions.h`**: Boost preconditions `min_ranks`, `power_of_two_ranks`,
`even_ranks`, `linear_routing`, `default_routing`.
- **`TestData.{h,cpp}`**: the `CaseData` struct and msgpack fixture loader.
- **`boost-test.cmake` / `boostAddTests.cmake`**: CMake test discovery.

Expand Down Expand Up @@ -117,7 +123,9 @@ name and cannot address suite-nested cases, tests use flat
(MPI-only), `partition_equivalence_tests.cpp`,
`mpi_distributed_layer_equivalence.cpp`, `mpi_fresh_insert_equivalence.cpp`
(serial↔world equivalence of the Schrödinger fused-resolve fresh-insert arms,
Majorana + native Pauli; self-skips at world size 1).
Majorana + native Pauli; self-skips at world size 1),
`mpi_routing_geometry_tests.cpp` (linear routing refuses a 3-rank
communicator).
- **Simulator / operator lifecycle**: `simulator_copy_tests.cpp`,
`update_initial_operator.cpp`, `ctor_validation_tests.cpp` (constructor guard
rails + MPGraph bounds).
Expand All @@ -134,16 +142,15 @@ CMake wraps the whole suite in `mpiexec -n <rank>` for each rank in
`monoprop_MPI_TEST_PROCS` (default `2;4`) — one CTest entry per rank count, not
per case, because the ranks have to reach the same collectives. Each entry has
a 600-second timeout, so a collective deadlock fails instead of occupying the
runner indefinitely. For exhaustive
rank coverage, run `just test-mpi '1;2;4'` or configure
`-Dmonoprop_MPI_TEST_PROCS='1;2;4'`. To run a single case under MPI while
runner indefinitely. `just test-mpi` runs the same entries CI does. To run a single case under MPI while
debugging, invoke the binary directly:
`mpirun -n 2 build/editable/Release-mpi/bin/monoprop_unit_tests.x --run_test=<case>`.

## Adding New Tests

1. Add a `*.cpp` with flat `BOOST_AUTO_TEST_CASE`s (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`.
3. For a case that needs several ranks, add a precondition:
`BOOST_AUTO_TEST_CASE(name, *test_utils::min_ranks(2))`. Never `return` early:
that passes without checking anything.
4. Rebuild to register the new cases with CTest.
89 changes: 89 additions & 0 deletions cpp/tests/RankPreconditions.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright 2026 Algorithmiq
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#pragma once

#include <bit>
#include <cstdlib>
#include <format>
#include <functional>
#include <string>
#include <utility>

#include <boost/test/unit_test.hpp>

#include "monoprop/detail/mpi/MPICompat.h"
#include "monoprop/detail/mpi/Routing.h"

/*
* Boost.Test preconditions for cases that need a particular MPI world. A case whose precondition
* fails is reported as skipped, with the reason, instead of passing without checking anything;
* CTest turns that report into a skip for the per-case serial entries.
*/
namespace test_utils {

namespace rank_detail {

inline auto world_size_precondition(std::function<bool(int)> accepts, std::string need)
-> boost::unit_test::decorator::precondition {
return boost::unit_test::precondition(
[accepts = std::move(accepts), need = std::move(need)](boost::unit_test::test_unit_id) {
const int ranks = monoprop::mpi::size(MPI_COMM_WORLD);
boost::test_tools::assertion_result result(accepts(ranks));
result.message() << "needs " << need << ", MPI_COMM_WORLD has " << ranks;
return result;
});
}

} // namespace rank_detail

/// Requires at least @p count ranks in MPI_COMM_WORLD.
inline auto min_ranks(int count) -> boost::unit_test::decorator::precondition {
return rank_detail::world_size_precondition([count](int ranks) { return ranks >= count; },
std::format("at least {} ranks", count));
}

/// Requires a power-of-two rank count of at least @p count, which the XOR pairing needs.
inline auto power_of_two_ranks(int count = 2) -> boost::unit_test::decorator::precondition {
return rank_detail::world_size_precondition(
[count](int ranks) { return ranks >= count && std::has_single_bit(static_cast<unsigned>(ranks)); },
std::format("a power-of-two rank count of at least {}", count));
}

/// Requires an even rank count, so that `rank ^ 1` pairs every rank.
inline auto even_ranks() -> boost::unit_test::decorator::precondition {
return rank_detail::world_size_precondition([](int ranks) { return ranks >= 2 && ranks % 2 == 0; },
"an even rank count");
}

/// Requires monoprop_ROUTING to resolve to linear routing.
inline auto linear_routing() -> boost::unit_test::decorator::precondition {
return boost::unit_test::precondition([](boost::unit_test::test_unit_id) {
boost::test_tools::assertion_result result(monoprop::routing::linear_requested());
result.message() << "needs linear routing, but monoprop_ROUTING selects splitmix";
return result;
});
}

/// Requires monoprop_ROUTING to be unset, so that the shipped default is what runs.
inline auto default_routing() -> boost::unit_test::decorator::precondition {
return boost::unit_test::precondition([](boost::unit_test::test_unit_id) {
const char *mode = std::getenv("monoprop_ROUTING");
boost::test_tools::assertion_result result(mode == nullptr || *mode == '\0');
result.message() << "needs monoprop_ROUTING unset, but it is set";
return result;
});
}

} // namespace test_utils
12 changes: 12 additions & 0 deletions cpp/tests/boost-test.cmake
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
# 4 as well as 2: at R = 2 the only shifts are 0 and 1, so a non-zero shift always pairs a rank with
# its single neighbour and no case distinguishes the shift from the rank index. R = 4 gives shifts
# 0..3 and a dense pairwise round of 2 * (R - 1) posts, which R = 2 cannot tell from a single leg.
# Of these defaults only 4 runs mpi_routing_geometry_tests.cpp, which needs at least 3 ranks
# under linear routing.
set(
monoprop_MPI_TEST_PROCS
"2;4"
CACHE STRING
"Semicolon-separated list of ranks for MPI test variants"
)

# Linear routing, the default, refuses a rank count that is not a power of two, so 3 ranks run
# only here; 2 and 4 rerun the dense all-to-all at the counts linear routing also covers.
set(
monoprop_MPI_SPLITMIX_TEST_PROCS
"2;3;4"
CACHE STRING
"Semicolon-separated list of ranks for the MPI test variants run with monoprop_ROUTING=splitmix"
)
set(_monoprop_mpiexec "${MPIEXEC_EXECUTABLE}")
if(NOT _monoprop_mpiexec)
find_program(
Expand Down Expand Up @@ -73,6 +84,7 @@ function(discover_tests TARGET)
"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
"TEST_MPI_SPLITMIX_NUMPROCS=${monoprop_MPI_SPLITMIX_TEST_PROCS}" -D
"MPIEXEC_EXECUTABLE=${_monoprop_mpiexec}" -D
"MPIEXEC_NUMPROC_FLAG=${_monoprop_mpiexec_numproc_flag}" -D
"MPIEXEC_PREFLAGS=${MPIEXEC_PREFLAGS}" -D
Expand Down
Loading
Loading