monoprop

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:

uv run python -m pytest -m "not mpi"   # or: just test

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          # full suite under MPI
just test-mpi-matrix   # MPI-marked tests across a rank matrix

To 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 SKBUILD_CMAKE_ARGS:

SKBUILD_CMAKE_ARGS="-Dmonoprop_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-mpi

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-failure

With MPI, reuse the MPI-enabled uv sync from the Python MPI section above, then run CTest against the same tree:

SKBUILD_CMAKE_ARGS="-Dmonoprop_ENABLE_MPI=ON" \
    uv sync --all-extras --reinstall-package monoprop --no-cache
ctest --test-dir build/editable/Release --output-on-failure

Or 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 -VV

A 64-bit TermIndex build is the only configuration that compiles the monoprop_WIDE_TERM_INDEX branches, so it has its own recipe:

just test-wide   # rebuilds via uv sync with monoprop_WIDE_TERM_INDEX=ON, then runs CTest

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. Select either group with ctest --test-dir build/editable/Release -L serial (or -L mpi-2).

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)) == expected

Data-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-8

Tag 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

On this page