Testing
Running the tests for the Python and C++ API, with or without MPI.
The monoprop test suite requires installing the package from source. If you follow the installation guide in Building from source, you should have all the tools needed to run the tests.
Running the tests
Python tests
The Python tests run with pytest. Without MPI:
just test # build, then the Python and C++ suites
just test-py # the Python suite against whatever is installedThis collects two suites: monoprop's own (tests/) and the one belonging to the
monoprop-bench-tools workspace member (packages/monoprop-bench-tools/tests/).
The member has to be installed for the second to import, which is what the
workspace-test dependency group is for:
uv sync --all-groups --all-extras # or: uv sync --group workspace-test --all-extrasThe plain test group covers monoprop's suite alone; it is deliberately free of
workspace members so it can be installed against a built wheel in CI.
The MPI tests need an MPI-enabled build, which the default source build does not
produce. The just recipes build an MPI-enabled extension and launch the suite
under mpiexec:
just test-mpi # build with MPI, then run every leg
just test-py-mpi "1;2;4" -m mpi # MPI-marked tests across a rank matrixTo do it by hand, build with MPI on, then run under mpiexec with --no-sync
so each rank reuses that build. --reinstall-package and --no-cache force a
genuine rebuild, since uv does not key its build cache on the environment:
monoprop_ENABLE_MPI=ON \
uv sync --all-extras --reinstall-package monoprop --no-cache
mpiexec --allow-run-as-root -n 2 uv run --no-sync python -m pytest tests --with-mpimonoprop_ENABLE_MPI also provisions mpi4py as a build input; see
Building from source for why, and for the
config-settings equivalent. Installing the mpi extra alone only provides
mpi4py and does not enable C++ MPI support. Accordingly, the CI matrix has
explicit cache-free MPI build lanes on Linux x86-64, Linux ARM64, and macOS;
only those lanes run the suite under mpiexec.
C++ unit tests
The C++ tests run through CTest. The build tree is produced by uv sync (via
scikit-build-core); do not invoke cmake --preset to configure directly, as the
project requires scikit-build-core's build environment to configure correctly.
Once the tree exists, run the tests with:
uv sync --all-extras
ctest --test-dir build/editable/Release --output-on-failureWith MPI, reuse the MPI-enabled uv sync from the Python MPI section above,
then run CTest against the same tree:
monoprop_ENABLE_MPI=ON \
uv sync --all-extras --reinstall-package monoprop --no-cache
ctest --test-dir build/editable/Release --output-on-failureOr simply just test-mpi.
The full MPI rank matrix (several rank counts) is driven by a helper script:
ctest -S tools/ctest-mpi-matrix.cmake -VVCI gives every compiler and MPI combination a distinct setup-uv cache suffix, because uv's cache key does not include build environment variables.
CTest runs each Boost 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
SKBUILD_CMAKE_DEFINE="monoprop_TEST_EXCLUDE_MPI_FABRIC=OFF" to the MPI build above.
CTest registers every Boost 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-<n> —
one entry per rank count rather than per case, because the ranks have to reach
the same collectives. Each MPI entry times out after 600 seconds, preventing a
collective deadlock from occupying the runner indefinitely.
CI runs the non-MPI and MPI labels separately with --no-tests=error, so an MPI
lane fails if its MPI variant was not registered. Standalone MPI-enabled C++
test executables initialize MPI before constructing propagators and destroy the
propagators before finalizing MPI. Select the groups locally with
just test-cpp and just test-cpp-mpi, which are the same commands CI runs. The
just test-mpi '1;2;4' recipe configures and runs the corresponding C++ CTest
rank entries as well as the Python rank matrix.
Run just code-coverage to collect coverage in separate serial and MPI-enabled
Coverage builds and render monoprop-coverage/index.html. The MPI collection
runs the serial and MPI CTest labels plus the MPI-marked Python tests with two
ranks. Python ranks write parallel coverage shards, while C++ coverage uses
atomic gcov counters. The code-coverage-collect and
code-coverage-aggregate recipes also drive the QA workflow, which publishes
the combined reports to Codecov and SonarQube. MPI collection requires covered
lines in multiple detail/mpi sources so a silently serial build cannot pass as
MPI coverage.
Adding tests
Python tests
Place new test files under tests/, named test_<module>.py. We suggest following pytest guidelines.
Simple unit test
Use pytest.mark.parametrize for straightforward parametric tests:
import pytest
from monoprop.pauli import Pauli
class TestPauli:
def test_default_qubits_are_range(self):
p = Pauli("XYZ")
assert p.string == "XYZ"
assert p.qubits == (0, 1, 2)
@pytest.mark.parametrize(
("string", "expected"),
[
("IZ", Pauli("Z", 1)),
("ZI", Pauli("Z", 0)),
],
)
def test_identity_letters_dropped(self, string, expected):
assert Pauli(string, (0, 1)) == expectedData-driven integration test
Use parametrize_with_cases to test against the reference msgpack fixtures.
The cases.py module defines CasesFermionicProblem, which exposes all fixtures
in tests/data/. The serial_comm and comm fixtures are provided by conftest.py.
import pytest
from pytest_cases import parametrize_with_cases
from monoprop import MajoranaPropagator
from tests.cases import CasesFermionicProblem
@parametrize_with_cases("problem", cases=CasesFermionicProblem)
def test_energy_matches_reference(problem, comm):
mp = MajoranaPropagator(
problem.operator,
problem.monomial_circuit.initial_state,
cutoff=2 * problem.n_modes,
comm=comm,
)
mp.propagate(problem.monomial_circuit.to_circuit())
energy = mp.expectation_value()
assert abs(energy - problem.exact_expval) < 1e-8Tag filtering narrows which fixtures run in a given test:
@parametrize_with_cases(
"problem", cases=CasesFermionicProblem, has_tag="has_commutator_data"
)
def test_only_commutator_cases(problem, serial_comm): ...Adding a new msgpack fixture
If your test requires a new reference problem, add a .msgpack file to tests/data/ following the schema in tests/data/README.md, then register a new case in tests/cases.py:
from pytest_cases import case
class CasesFermionicProblem:
@case(id="my_new_problem", tags=["has_commutator_data"])
def case_my_new_problem(self, shared_datadir):
return load_problem(shared_datadir / "my_new_problem.msgpack")C++ tests
C++ tests live in cpp/tests/ and use Boost.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.
Simple unit test
#include <boost/test/unit_test.hpp>
BOOST_AUTO_TEST_CASE(my_basic_check) {
int result = 2 + 2;
BOOST_TEST(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:
#include <boost/test/data/monomorphic.hpp>
#include <boost/test/data/test_case.hpp>
#include <boost/test/unit_test.hpp>
#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) {
const auto schrodinger_cutoff = make_schrodinger_cutoff(sch_enabled, cutoff);
SimulatorConfig cfg{
.schrodinger_cutoff = schrodinger_cutoff
? std::optional<unsigned int>(*schrodinger_cutoff)
: std::nullopt,
.cutoff_type = cutoff_type,
.basis_change = basis_change,
};
// use data.actual_expval as the reference value
test_evolve_build_graph<n_modes>(data, cfg, pare, data.actual_expval);
}See also
- Getting Started — installing a prebuilt release from PyPI.
- Parallelism and distribution — running across MPI ranks and shared-memory threads.
- How to Contribute — the full test and documentation workflow.